Completed
Pull Request — master (#1371)
by
unknown
13:30 queued 04:58
created

SQLiteAdapter::connect()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.3329

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 12
cts 18
cp 0.6667
rs 8.439
c 0
b 0
f 0
cc 6
eloc 18
nc 6
nop 0
crap 7.3329
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Db\Adapter
28
 */
29
namespace Phinx\Db\Adapter;
30
31
use Cake\Database\Connection;
32
use Cake\Database\Driver\Sqlite as SqliteDriver;
33
use Phinx\Db\Table\Column;
34
use Phinx\Db\Table\ForeignKey;
35
use Phinx\Db\Table\Index;
36
use Phinx\Db\Table\Table;
37
use Phinx\Db\Util\AlterInstructions;
38
use Phinx\Util\Literal;
39
40
/**
41
 * Phinx SQLite Adapter.
42
 *
43
 * @author Rob Morgan <[email protected]>
44
 * @author Richard McIntyre <[email protected]>
45
 */
46
class SQLiteAdapter extends PdoAdapter implements AdapterInterface
47
{
48
    protected $definitionsWithLimits = [
49
        'CHARACTER',
50
        'VARCHAR',
51
        'VARYING CHARACTER',
52
        'NCHAR',
53
        'NATIVE CHARACTER',
54
        'NVARCHAR'
55
    ];
56 42
57
    protected $suffix = '.sqlite3';
58 42
59 42
    /**
60
     * {@inheritdoc}
61
     */
62
    public function connect()
63
    {
64
        if ($this->connection === null) {
65 42
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
66 42
                // @codeCoverageIgnoreStart
67
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
68
                // @codeCoverageIgnoreEnd
69 42
            }
70
71
            $db = null;
72 42
            $options = $this->getOptions();
73 42
74 42
            // if port is specified use it, otherwise use the MySQL default
75 42
            if (isset($options['memory'])) {
76
                $dsn = 'sqlite::memory:';
77
            } else {
78
                $dsn = 'sqlite:' . $options['name'].$this->suffix;
79 42
            }
80 42
81
            try {
82
                $db = new \PDO($dsn);
83
            } catch (\PDOException $exception) {
84
                throw new \InvalidArgumentException(sprintf(
85
                    'There was a problem connecting to the database: %s',
86
                    $exception->getMessage()
87 42
                ));
88 42
            }
89 42
90
            $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
91
            $this->setConnection($db);
92
        }
93
    }
94 48
95
    /**
96 48
     * {@inheritdoc}
97 48
     */
98
    public function setOptions(array $options)
99
    {
100
        parent::setOptions($options);
101
102
        if (isset($options['suffix'])) {
103
            $this->suffix = $options['suffix'];
104
        }
105
        if (substr($this->suffix, 0, 1) !== '.') {
106
            $this->suffix = '.' . $this->suffix;
107
        }
108
109
        return $this;
110 1
    }
111
112 1
    /**
113 1
     * {@inheritdoc}
114
     */
115
    public function disconnect()
116
    {
117
        $this->connection = null;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function hasTransactions()
124
    {
125
        return true;
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function beginTransaction()
132
    {
133
        $this->getConnection()->beginTransaction();
134 43
    }
135
136 43
    /**
137
     * {@inheritdoc}
138
     */
139
    public function commitTransaction()
140
    {
141
        $this->getConnection()->commit();
142 44
    }
143
144 44
    /**
145
     * {@inheritdoc}
146
     */
147
    public function rollbackTransaction()
148
    {
149
        $this->getConnection()->rollBack();
150 42
    }
151
152 42
    /**
153 42
     * {@inheritdoc}
154 42
     */
155 12
    public function quoteTableName($tableName)
156 42
    {
157
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
158 42
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163
    public function quoteColumnName($columnName)
164 42
    {
165
        return '`' . str_replace('`', '``', $columnName) . '`';
166
    }
167 42
168 42
    /**
169 42
     * {@inheritdoc}
170 35
     */
171 35
    public function hasTable($tableName)
172 35
    {
173 35
        $tables = [];
174
        $rows = $this->fetchAll(sprintf('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'%s\'', $tableName));
175 35
        foreach ($rows as $row) {
176 42
            $tables[] = strtolower($row[0]);
177
        }
178 1
179 1
        return in_array(strtolower($tableName), $tables);
180 1
    }
181 1
182
    /**
183 1
     * {@inheritdoc}
184 1
     */
185
    public function createTable(Table $table, array $columns = [], array $indexes = [])
186
    {
187 42
        // Add the default primary key
188 42
        $options = $table->getOptions();
189 42
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
190 42
            $column = new Column();
191 42
            $column->setName('id')
192
                   ->setType('integer')
193
                   ->setIdentity(true);
194 42
195 42
            array_unshift($columns, $column);
196 42
        } elseif (isset($options['id']) && is_string($options['id'])) {
197 42
            // Handle id => "field_name" to support AUTO_INCREMENT
198 42
            $column = new Column();
199 42
            $column->setName($options['id'])
200
                   ->setType('integer')
201
                   ->setIdentity(true);
202 1
203 1
            array_unshift($columns, $column);
204 1
        }
205
206 1
        $sql = 'CREATE TABLE ';
207 1
        $sql .= $this->quoteTableName($table->getName()) . ' (';
208 1
        foreach ($columns as $column) {
209 1
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
210 1
211 1
            if (isset($options['primary_key']) && $column->getIdentity()) {
212 42
                //remove column from the primary key array as it is already defined as an autoincrement
213 42
                //primary id
214 37
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
215
                if ($identityColumnIndex !== false) {
216
                    unset($options['primary_key'][$identityColumnIndex]);
217
218 42
                    if (empty($options['primary_key'])) {
219 42
                        //The last primary key has been removed
220 1
                        unset($options['primary_key']);
221 1
                    }
222 1
                }
223 1
            }
224
        }
225 42
226
        // set the primary key(s)
227 42 View Code Duplication
        if (isset($options['primary_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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