Completed
Pull Request — master (#1357)
by José
11:07
created

SQLiteAdapter::getDecoratedConnection()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 13
cts 13
cp 1
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 14
nc 6
nop 0
crap 4
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
    /**
58 42
     * {@inheritdoc}
59 42
     */
60
    public function connect()
61
    {
62
        if ($this->connection === null) {
63
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
64
                // @codeCoverageIgnoreStart
65 42
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
66 42
                // @codeCoverageIgnoreEnd
67
            }
68
69 42
            $db = null;
70
            $options = $this->getOptions();
71
72 42
            // if port is specified use it, otherwise use the MySQL default
73 42
            if (isset($options['memory'])) {
74 42
                $dsn = 'sqlite::memory:';
75 42
            } else {
76
                $dsn = 'sqlite:' . $options['name'];
77
                if (file_exists($options['name'] . '.sqlite3')) {
78
                    $dsn = 'sqlite:' . $options['name'] . '.sqlite3';
79 42
                }
80 42
            }
81
82
            try {
83
                $db = new \PDO($dsn);
84
            } catch (\PDOException $exception) {
85
                throw new \InvalidArgumentException(sprintf(
86
                    'There was a problem connecting to the database: %s',
87 42
                    $exception->getMessage()
88 42
                ));
89 42
            }
90
91
            $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
92
            $this->setConnection($db);
93
        }
94 48
    }
95
96 48
    /**
97 48
     * {@inheritdoc}
98
     */
99
    public function disconnect()
100
    {
101
        $this->connection = null;
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function hasTransactions()
108
    {
109
        return true;
110 1
    }
111
112 1
    /**
113 1
     * {@inheritdoc}
114
     */
115
    public function beginTransaction()
116
    {
117
        $this->getConnection()->beginTransaction();
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123
    public function commitTransaction()
124
    {
125
        $this->getConnection()->commit();
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131
    public function rollbackTransaction()
132
    {
133
        $this->getConnection()->rollBack();
134 43
    }
135
136 43
    /**
137
     * {@inheritdoc}
138
     */
139
    public function quoteTableName($tableName)
140
    {
141
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
142 44
    }
143
144 44
    /**
145
     * {@inheritdoc}
146
     */
147
    public function quoteColumnName($columnName)
148
    {
149
        return '`' . str_replace('`', '``', $columnName) . '`';
150 42
    }
151
152 42
    /**
153 42
     * {@inheritdoc}
154 42
     */
155 12
    public function hasTable($tableName)
156 42
    {
157
        $tables = [];
158 42
        $rows = $this->fetchAll(sprintf('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'%s\'', $tableName));
159
        foreach ($rows as $row) {
160
            $tables[] = strtolower($row[0]);
161
        }
162
163
        return in_array(strtolower($tableName), $tables);
164 42
    }
165
166
    /**
167 42
     * {@inheritdoc}
168 42
     */
169 42
    public function createTable(Table $table, array $columns = [], array $indexes = [])
170 35
    {
171 35
        // Add the default primary key
172 35
        $options = $table->getOptions();
173 35
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
174
            $column = new Column();
175 35
            $column->setName('id')
176 42
                   ->setType('integer')
177
                   ->setIdentity(true);
178 1
179 1
            array_unshift($columns, $column);
180 1
        } elseif (isset($options['id']) && is_string($options['id'])) {
181 1
            // Handle id => "field_name" to support AUTO_INCREMENT
182
            $column = new Column();
183 1
            $column->setName($options['id'])
184 1
                   ->setType('integer')
185
                   ->setIdentity(true);
186
187 42
            array_unshift($columns, $column);
188 42
        }
189 42
190 42
        $sql = 'CREATE TABLE ';
191 42
        $sql .= $this->quoteTableName($table->getName()) . ' (';
192
        foreach ($columns as $column) {
193
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
194 42
195 42
            if (isset($options['primary_key']) && $column->getIdentity()) {
196 42
                //remove column from the primary key array as it is already defined as an autoincrement
197 42
                //primary id
198 42
                $identityColumnIndex = array_search($column->getName(), $options['primary_key']);
199 42
                if ($identityColumnIndex !== false) {
200
                    unset($options['primary_key'][$identityColumnIndex]);
201
202 1
                    if (empty($options['primary_key'])) {
203 1
                        //The last primary key has been removed
204 1
                        unset($options['primary_key']);
205
                    }
206 1
                }
207 1
            }
208 1
        }
209 1
210 1
        // set the primary key(s)
211 1 View Code Duplication
        if (isset($options['primary_key'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
212 42
            $sql = rtrim($sql);
213 42
            $sql .= ' PRIMARY KEY (';
214 37
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
215
                $sql .= $this->quoteColumnName($options['primary_key']);
216
            } 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...
217
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
218 42
            }
219 42
            $sql .= ')';
220 1
        } else {
221 1
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
222 1
        }
223 1
224
        $sql = rtrim($sql) . ');';
225 42
        // execute the sql
226
        $this->execute($sql);
227 42
228
        foreach ($indexes as $index) {
229 42
            $this->addIndex($table, $index);
230 6
        }
231 42
    }
232 42
233
    /**
234
     * {@inheritdoc}
235
     */
236 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...
237 1
    {
238
        $sql = sprintf(
239 1
            'ALTER TABLE %s RENAME TO %s',
240 1
            $this->quoteTableName($tableName),
241
            $this->quoteTableName($newTableName)
242
        );
243
244
        return new AlterInstructions([], [$sql]);
245 1
    }
246
247 1
    /**
248 1
     * {@inheritdoc}
249
     */
250
    protected function getDropTableInstructions($tableName)
251
    {
252
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
253 1
254
        return new AlterInstructions([], [$sql]);
255 1
    }
256 1
257 1
    /**
258 1
     * {@inheritdoc}
259
     */
260 1
    public function truncateTable($tableName)
261 1
    {
262
        $sql = sprintf(
263
            'DELETE FROM %s',
264
            $this->quoteTableName($tableName)
265
        );
266 1
267
        $this->execute($sql);
268 1
    }
269 1
270
    /**
271 1
     * {@inheritdoc}
272 1
     */
273 1
    public function getColumns($tableName)
274 1
    {
275 1
        $columns = [];
276 1
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
277
278 1
        foreach ($rows as $columnInfo) {
279 1
            $column = new Column();
280 1
            $type = strtolower($columnInfo['type']);
281
            $column->setName($columnInfo['name'])
282 1
                   ->setNull($columnInfo['notnull'] !== '1')
283 1
                   ->setDefault($columnInfo['dflt_value']);
284 1
285
            $phinxType = $this->getPhinxType($type);
286 1
            $column->setType($phinxType['name'])
287 1
                   ->setLimit($phinxType['limit']);
288
289 1
            if ($columnInfo['pk'] == 1) {
290
                $column->setIdentity(true);
291
            }
292
293
            $columns[] = $column;
294
        }
295 8
296
        return $columns;
297 8
    }
298 8
299 8
    /**
300 7
     * {@inheritdoc}
301
     */
302 8 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...
303
    {
304 8
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
305
        foreach ($rows as $column) {
306
            if (strcasecmp($column['name'], $columnName) === 0) {
307
                return true;
308
            }
309
        }
310 4
311
        return false;
312 4
    }
313 4
314 4
    /**
315 4
     * {@inheritdoc}
316 4
     */
317 4 View Code Duplication
    protected function getAddColumnInstructions(Table $table, Column $column)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
318
    {
319 4
        $alter = sprintf(
320 4
            'ADD COLUMN %s %s',
321
            $this->quoteColumnName($column->getName()),
322
            $this->getColumnSqlDefinition($column)
323
        );
324
325 2
        return new AlterInstructions([$alter]);
326
    }
327 2
328
    /**
329 2
     * Returns the original CREATE statement for the give table
330
     *
331 2
     * @param string $tableName The table name to get the create statement for
332 2
     * @return string
333 2
     */
334 2
    protected function getDeclaringSql($tableName)
335 2
    {
336 2
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
337
338 2
        $sql = '';
339 2
        foreach ($rows as $table) {
340 2
            if ($table['tbl_name'] === $tableName) {
341 2
                $sql = $table['sql'];
342 2
            }
343 2
        }
344 2
345 2
        return $sql;
346 2
    }
347
348 2
    /**
349 1
     * Copies all the data from a tmp table to another table
350
     *
351 1
     * @param string $tableName The table name to copy the data to
352
     * @param string $tmpTableName The tmp table name where the data is stored
353
     * @param string[] $writeColumns The list of columns in the target table
354 1
     * @param string[] $selectColumns The list of columns in the tmp table
355
     * @return void
356 1
     */
357 1
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
358 1
    {
359
        $sql = sprintf(
360 1
            'INSERT INTO %s(%s) SELECT %s FROM %s',
361 1
            $this->quoteTableName($tableName),
362
            implode(', ', $writeColumns),
363
            implode(', ', $selectColumns),
364 1
            $this->quoteTableName($tmpTableName)
365 1
        );
366 1
        $this->execute($sql);
367 1
    }
368 1
369
    /**
370 1
     * Modifies the passed instructions to copy all data from the tmp table into
371
     * the provided table and then drops the tmp table.
372 1
     *
373
     * @param AlterInstructions $instructions The instructions to modify
374 1
     * @param string $tableName The table name to copy the data to
375 1
     * @return AlterInstructions
376
     */
377
    protected function copyAndDropTmpTable($instructions, $tableName)
378
    {
379
        $instructions->addPostStep(function ($state) use ($tableName) {
380 6
            $this->copyDataToNewTable(
381
                $tableName,
382
                $state['tmpTableName'],
383
                $state['writeColumns'],
384 6
                $state['selectColumns']
385
            );
386 6
387
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($state['tmpTableName'])));
388 6
389 6
            return $state;
390 6
        });
391 6
392 6
        return $instructions;
393 6
    }
394
395 6
    /**
396 6
     * Returns the columns and type to use when copying a table to another in the process
397 6
     * of altering a table
398 6
     *
399 6
     * @param string $tableName The table to modify
400 6
     * @param string $columnName The column name that is about to change
401 6
     * @param string|false $newColumnName Optionally the new name for the column
402 6
     * @return AlterInstructions
403 6
     */
404
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
405 6
    {
406
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
407
        $selectColumns = [];
408
        $writeColumns = [];
409
        $columnType = null;
410
        $found = false;
411 6
412
        foreach ($columns as $column) {
413 6
            $selectName = $column['name'];
414 6
            $writeName = $selectName;
415 6
416 6
            if ($selectName == $columnName) {
417
                $writeName = $newColumnName;
418 6
                $found = true;
419
                $columnType = $column['type'];
420 6
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
421
            }
422 6
423 6
            $selectColumns[] = $selectName;
424 6
            $writeColumns[] = $writeName;
425 6
        }
426 6
427
        $selectColumns = array_filter($selectColumns, 'strlen');
428 6
        $writeColumns = array_filter($writeColumns, 'strlen');
429
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
430 6
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
431 6
432 6
        if (!$found) {
433
            throw new \InvalidArgumentException(sprintf(
434
                'The specified column doesn\'t exist: ' . $columnName
435
            ));
436
        }
437 2
438
        return compact('writeColumns', 'selectColumns', 'columnType');
439
    }
440 2
441
    /**
442 2
     * Returns the initial instructions to alter a table using the
443
     * rename-alter-copy strategy
444 2
     *
445 2
     * @param string $tableName The table to modify
446 2
     * @return AlterInstructions
447 2
     */
448 2
    protected function beginAlterByCopyTable($tableName)
449 2
    {
450
        $instructions = new AlterInstructions();
451 2
        $instructions->addPostStep(function ($state) use ($tableName) {
452 2
            $createSQL = $this->getDeclaringSql($tableName);
453 2
454 2
            $tmpTableName = 'tmp_' . $tableName;
455 2
            $this->execute(
456 2
                sprintf(
457 2
                    'ALTER TABLE %s RENAME TO %s',
458 2
                    $this->quoteTableName($tableName),
459 2
                    $this->quoteTableName($tmpTableName)
460
                )
461 2
            );
462
463 2
            return compact('createSQL', 'tmpTableName') + $state;
464
        });
465
466
        return $instructions;
467
    }
468
469 2
    /**
470
     * {@inheritdoc}
471 2
     */
472 2
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
473 2
    {
474
        $instructions = $this->beginAlterByCopyTable($tableName);
475 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...
476
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
477 2
478 2
            return $newState + $state;
479 2
        });
480
481 2
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
482
            $sql = str_replace(
483 2
                $this->quoteColumnName($columnName),
484 2
                $this->quoteColumnName($newColumnName),
485 2
                $state['createSQL']
486 2
            );
487 2
            $this->execute($sql);
488
489 2
            return $state;
490
        });
491 2
492 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
493 2
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
499
    {
500
        $instructions = $this->beginAlterByCopyTable($tableName);
501 9
502
        $newColumnName = $newColumn->getName();
503 9 View Code Duplication
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
504 9
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, $newColumnName);
505
506 9
            return $newState + $state;
507 9
        });
508 9
509 9
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
510 9
            $sql = preg_replace(
511 9
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
512 9
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
513 9
                $state['createSQL'],
514 9
                1
515 9
            );
516
            $this->execute($sql);
517
518
            return $state;
519
        });
520
521 9
        return $this->copyAndDropTmpTable($instructions, $tableName);
522
    }
523 9
524 4
    /**
525 4
     * {@inheritdoc}
526
     */
527 9
    protected function getDropColumnInstructions($tableName, $columnName)
528 9
    {
529
        $instructions = $this->beginAlterByCopyTable($tableName);
530 9
531 9 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...
532 9
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columnName, false);
533 9
534
            return $newState + $state;
535 8
        });
536
537 8
        $instructions->addPostStep(function ($state) use ($columnName) {
538
            $sql = preg_replace(
539
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
540
                "",
541
                $state['createSQL']
542
            );
543 1
544
            if (substr($sql, -2) === ', ') {
545 1
                $sql = substr($sql, 0, -2) . ')';
546
            }
547 1
548 1
            $this->execute($sql);
549 1
550
            return $state;
551
        });
552
553
        return $this->copyAndDropTmpTable($instructions, $tableName);
554
    }
555
556
    /**
557
     * Get an array of indexes from a particular table.
558
     *
559 8
     * @param string $tableName Table Name
560
     * @return array
561 8
     */
562 8
    protected function getIndexes($tableName)
563 8
    {
564 8
        $indexes = [];
565 8
        $rows = $this->fetchAll(sprintf('pragma index_list(%s)', $tableName));
566 8
567 8
        foreach ($rows as $row) {
568 8
            $indexData = $this->fetchAll(sprintf('pragma index_info(%s)', $row['name']));
569 8
            if (!isset($indexes[$tableName])) {
570 8
                $indexes[$tableName] = ['index' => $row['name'], 'columns' => []];
571
            }
572 8
            foreach ($indexData as $indexItem) {
573 8
                $indexes[$tableName]['columns'][] = strtolower($indexItem['name']);
574 8
            }
575
        }
576
577
        return $indexes;
578
    }
579 1
580
    /**
581 1
     * {@inheritdoc}
582 1
     */
583 1 View Code Duplication
    public function hasIndex($tableName, $columns)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
584
    {
585 1
        if (is_string($columns)) {
586 1
            $columns = [$columns]; // str to array
587
        }
588 1
589 1
        $columns = array_map('strtolower', $columns);
590 1
        $indexes = $this->getIndexes($tableName);
591 1
592 1
        foreach ($indexes as $index) {
593 1
            $a = array_diff($columns, $index['columns']);
594 1
            if (empty($a)) {
595 1
                return true;
596 1
            }
597 1
        }
598
599
        return false;
600
    }
601
602
    /**
603
     * {@inheritdoc}
604
     */
605 1 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...
606
    {
607 1
        $indexes = $this->getIndexes($tableName);
608
609 1
        foreach ($indexes as $index) {
610 1
            if ($indexName === $index['index']) {
611 1
                return true;
612 1
            }
613 1
        }
614 1
615 1
        return false;
616 1
    }
617 1
618
    /**
619
     * {@inheritdoc}
620
     */
621
    protected function getAddIndexInstructions(Table $table, Index $index)
622
    {
623
        $indexColumnArray = [];
624
        foreach ($index->getColumns() as $column) {
625 5
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
626
        }
627 5
        $indexColumns = implode(',', $indexColumnArray);
628
        $sql = sprintf(
629
            'CREATE %s ON %s (%s)',
630 5
            $this->getIndexSqlDefinition($table, $index),
631
            $this->quoteTableName($table->getName()),
632 5
            $indexColumns
633 5
        );
634 5
635
        return new AlterInstructions([], [$sql]);
636 1
    }
637
638
    /**
639
     * {@inheritdoc}
640
     */
641
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
642
    {
643
        if (is_string($columns)) {
644
            $columns = [$columns]; // str to array
645 5
        }
646
647 5
        $indexes = $this->getIndexes($tableName);
648 5
        $columns = array_map('strtolower', $columns);
649
        $instructions = new AlterInstructions();
650
651
        foreach ($indexes as $index) {
652
            $a = array_diff($columns, $index['columns']);
653
            if (empty($a)) {
654
                $instructions->addPostStep(sprintf(
655
                    'DROP INDEX %s',
656
                    $this->quoteColumnName($index['index'])
657
                ));
658
            }
659
        }
660
661 5
        return $instructions;
662
    }
663 5
664 5
    /**
665 5
     * {@inheritdoc}
666 5
     */
667 5
    protected function getDropIndexByNameInstructions($tableName, $indexName)
668 5
    {
669 5
        $indexes = $this->getIndexes($tableName);
670 5
        $instructions = new AlterInstructions();
671 5
672 5
        foreach ($indexes as $index) {
673 5
            if ($indexName === $index['index']) {
674
                $instructions->addPostStep(sprintf(
675
                    'DROP INDEX %s',
676
                    $this->quoteColumnName($indexName)
677
                ));
678
            }
679 4
        }
680
681
        return $instructions;
682 4
    }
683
684 4
    /**
685 4
     * {@inheritdoc}
686
     */
687 4
    public function hasForeignKey($tableName, $columns, $constraint = null)
688 4
    {
689 4
        if (is_string($columns)) {
690 4
            $columns = [$columns]; // str to array
691 4
        }
692 4
        $foreignKeys = $this->getForeignKeys($tableName);
693
694 4
        return !array_diff($columns, $foreignKeys);
695 4
    }
696 4
697 4
    /**
698 4
     * Get an array of foreign keys from a particular table.
699
     *
700 4
     * @param string $tableName Table Name
701
     * @return array
702 4
     */
703 4
    protected function getForeignKeys($tableName)
704
    {
705 4
        $foreignKeys = [];
706 4
        $rows = $this->fetchAll(
707 4
            "SELECT sql, tbl_name
708 4
              FROM (
709 4
                    SELECT sql sql, type type, tbl_name tbl_name, name name
710 4
                      FROM sqlite_master
711 4
                     UNION ALL
712
                    SELECT sql, type, tbl_name, name
713 4
                      FROM sqlite_temp_master
714 4
                   )
715 4
             WHERE type != 'meta'
716
               AND sql NOTNULL
717
               AND name NOT LIKE 'sqlite_%'
718
             ORDER BY substr(type, 2, 1), name"
719
        );
720 1
721
        foreach ($rows as $row) {
722
            if ($row['tbl_name'] === $tableName) {
723 1
                if (strpos($row['sql'], 'REFERENCES') !== false) {
724
                    preg_match_all("/\(`([^`]*)`\) REFERENCES/", $row['sql'], $matches);
725
                    foreach ($matches[1] as $match) {
726
                        $foreignKeys[] = $match;
727 1
                    }
728
                }
729 1
            }
730
        }
731 1
732 1
        return $foreignKeys;
733 1
    }
734 1
735 1
    /**
736 1
     * {@inheritdoc}
737
     */
738 1
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
739 1
    {
740 1
        $instructions = $this->beginAlterByCopyTable($table->getName());
741 1
742 1
        $tableName = $table->getName();
743 1
        $instructions->addPostStep(function ($state) use ($foreignKey) {
744 1
            $this->execute('pragma foreign_keys = ON');
745
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . ')';
746 1
            $this->execute($sql);
747
748 1
            return $state;
749
        });
750
751
        $instructions->addPostStep(function ($state) {
752
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
753
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
754 1
            $selectColumns = $writeColumns = $names;
755
756 1
            return compact('selectColumns', 'writeColumns') + $state;
757 1
        });
758 1
759 1
        return $this->copyAndDropTmpTable($instructions, $tableName);
760 1
    }
761 1
762 1
    /**
763
     * {@inheritdoc}
764 1
     */
765
    protected function getDropForeignKeyInstructions($tableName, $constraint)
766 1
    {
767 1
        throw new \BadMethodCallException('SQLite does not have named foreign keys');
768 1
    }
769 1
770 1
    /**
771
     * {@inheritdoc}
772 1
     */
773
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
774 1
    {
775 1
        $instructions = $this->beginAlterByCopyTable($tableName);
776 1
777
        $instructions->addPostStep(function ($state) use ($columns) {
778
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
779
780
            $selectColumns = $newState['selectColumns'];
781
            $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...
782
            $diff = array_diff($columns, $selectColumns);
783
784
            if (!empty($diff)) {
785
                throw new \InvalidArgumentException(sprintf(
786
                    'The specified columns doen\'t exist: ' . implode(', ', $diff)
787
                ));
788
            }
789
790
            return $newState + $state;
791
        });
792
793
        $instructions->addPostStep(function ($state) use ($columns) {
794
            $sql = '';
795
796
            foreach ($columns as $columnName) {
797
                $search = sprintf(
798
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
799
                    $this->quoteColumnName($columnName)
800
                );
801
                $sql = preg_replace($search, '', $state['createSQL'], 1);
802
            }
803
804
            if ($sql) {
805
                $this->execute($sql);
806
            }
807
808
            return $state;
809
        });
810 43
811
        return $this->copyAndDropTmpTable($instructions, $tableName);
812
    }
813 43
814 42
    /**
815
     * {@inheritdoc}
816 43
     */
817
    public function getSqlType($type, $limit = null)
818
    {
819 43
        switch ($type) {
820 1
            case static::PHINX_TYPE_TEXT:
821
            case static::PHINX_TYPE_INTEGER:
822 43
            case static::PHINX_TYPE_FLOAT:
823 38
            case static::PHINX_TYPE_DECIMAL:
824
            case static::PHINX_TYPE_DATETIME:
825 43
            case static::PHINX_TYPE_TIME:
826 42
            case static::PHINX_TYPE_DATE:
827
            case static::PHINX_TYPE_BLOB:
828 43
            case static::PHINX_TYPE_BOOLEAN:
829 2
            case static::PHINX_TYPE_ENUM:
830
                return ['name' => $type];
831 43
            case static::PHINX_TYPE_STRING:
832 1
                return ['name' => 'varchar', 'limit' => 255];
833
            case static::PHINX_TYPE_CHAR:
834 43
                return ['name' => 'char', 'limit' => 255];
835 1
            case static::PHINX_TYPE_BIG_INTEGER:
836
                return ['name' => 'bigint'];
837 43
            case static::PHINX_TYPE_TIMESTAMP:
838 42
                return ['name' => 'datetime'];
839
            case static::PHINX_TYPE_BINARY:
840 43
                return ['name' => 'blob'];
841 1
            case static::PHINX_TYPE_UUID:
842
                return ['name' => 'char', 'limit' => 36];
843 43
            case static::PHINX_TYPE_JSON:
844 1
            case static::PHINX_TYPE_JSONB:
845
                return ['name' => 'text'];
846 43
            // Geospatial database types
847 43
            // No specific data types exist in SQLite, instead all geospatial
848 1
            // functionality is handled in the client. See also: SpatiaLite.
849
            case static::PHINX_TYPE_GEOMETRY:
850 43
            case static::PHINX_TYPE_POLYGON:
851 42
                return ['name' => 'text'];
852
            case static::PHINX_TYPE_LINESTRING:
853 5
                return ['name' => 'varchar', 'limit' => 255];
854
            case static::PHINX_TYPE_POINT:
855 5
                return ['name' => 'float'];
856 4
            default:
857
                throw new \RuntimeException('The type: "' . $type . '" is not supported.');
858
        }
859
    }
860 1
861 1
    /**
862
     * Returns Phinx type by SQL type
863
     *
864 1
     * @param string $sqlTypeDef SQL type
865
     * @returns string Phinx type
866
     */
867 1
    public function getPhinxType($sqlTypeDef)
868
    {
869 1
        if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*$/', $sqlTypeDef, $matches)) {
870 1
            throw new \RuntimeException('Column type ' . $sqlTypeDef . ' is not supported');
871 1
        } else {
872
            $limit = null;
873
            $precision = null;
874
            $type = $matches[1];
875
            if (count($matches) > 2) {
876
                $limit = $matches[3] ?: null;
877
            }
878
            if (count($matches) > 4) {
879
                $precision = $matches[5];
880 3
            }
881
            switch ($matches[1]) {
882 3
                case 'varchar':
883 1
                    $type = static::PHINX_TYPE_STRING;
884
                    if ($limit === 255) {
885 2
                        $limit = null;
886 2
                    }
887 2
                    break;
888 2 View Code Duplication
                case 'char':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
889 1
                    $type = static::PHINX_TYPE_CHAR;
890 1
                    if ($limit === 255) {
891 2
                        $limit = null;
892
                    }
893
                    if ($limit === 36) {
894 2
                        $type = static::PHINX_TYPE_UUID;
895 2
                    }
896 1
                    break;
897 1
                case 'int':
898
                    $type = static::PHINX_TYPE_INTEGER;
899
                    if ($limit === 11) {
900 1
                        $limit = null;
901 2
                    }
902
                    break;
903
                case 'bigint':
904
                    if ($limit === 11) {
905
                        $limit = null;
906
                    }
907
                    $type = static::PHINX_TYPE_BIG_INTEGER;
908
                    break;
909
                case 'blob':
910 2
                    $type = static::PHINX_TYPE_BINARY;
911
                    break;
912
            }
913 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...
914
                if ($matches[3] === 1) {
915
                    $type = static::PHINX_TYPE_BOOLEAN;
916 2
                    $limit = null;
917 1
                }
918
            }
919
920 1
            $this->getSqlType($type);
921 1
922 2
            return [
923 1
                'name' => $type,
924 1
                'limit' => $limit,
925 2
                'precision' => $precision
926 2
            ];
927
        }
928
    }
929
930
    /**
931
     * {@inheritdoc}
932
     */
933 2
    public function createDatabase($name, $options = [])
934
    {
935
        touch($name . '.sqlite3');
936 1
    }
937 1
938
    /**
939 1
     * {@inheritdoc}
940
     */
941
    public function hasDatabase($name)
942
    {
943
        return is_file($name . '.sqlite3');
944
    }
945
946 48
    /**
947
     * {@inheritdoc}
948 48
     */
949 48
    public function dropDatabase($name)
950
    {
951
        if (file_exists($name . '.sqlite3')) {
952
            unlink($name . '.sqlite3');
953
        }
954 2
    }
955
956 2
    /**
957
     * Gets the SQLite Column Definition for a Column object.
958
     *
959
     * @param \Phinx\Db\Table\Column $column Column
960
     * @return string
961
     */
962 48
    protected function getColumnSqlDefinition(Column $column)
963
    {
964 48
        $isLiteralType = $column->getType() instanceof Literal;
965 47
        if ($isLiteralType) {
966 47
            $def = (string)$column->getType();
967 48
        } else {
968
            $sqlType = $this->getSqlType($column->getType());
969
            $def = strtoupper($sqlType['name']);
970
971
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits);
972
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
973
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
974
            }
975 42
        }
976 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...
977 42
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
978 8
        }
979 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...
980 42
            $def .= " CHECK({$column->getName()} IN ('" . implode("', '", $values) . "'))";
981 42
        }
982 42
983
        $default = $column->getDefault();
984
985
        $def .= (!$column->isIdentity() && ($column->isNull() || is_null($default))) ? ' NULL' : ' NOT NULL';
986
        $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...
987
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
988
989
        if ($column->getUpdate()) {
990
            $def .= ' ON UPDATE ' . $column->getUpdate();
991 42
        }
992
993 42
        $def .= $this->getCommentDefinition($column);
994 42
995 42
        return $def;
996 42
    }
997
998
    /**
999 42
     * Gets the comment Definition for a Column object.
1000 42
     *
1001 42
     * @param \Phinx\Db\Table\Column $column Column
1002 42
     * @return string
1003 42
     */
1004 4
    protected function getCommentDefinition(Column $column)
1005 4
    {
1006
        if ($column->getComment()) {
1007 42
            return ' /* ' . $column->getComment() . ' */ ';
1008
        }
1009 42
1010 42
        return '';
1011 42
    }
1012
1013 42
    /**
1014
     * Gets the SQLite Index Definition for an Index object.
1015
     *
1016
     * @param \Phinx\Db\Table\Table $table Table
1017 42
     * @param \Phinx\Db\Table\Index $index Index
1018
     * @return string
1019 42
     */
1020
    protected function getIndexSqlDefinition(Table $table, Index $index)
1021
    {
1022
        if ($index->getType() === Index::UNIQUE) {
1023
            $def = 'UNIQUE INDEX';
1024
        } else {
1025
            $def = 'INDEX';
1026
        }
1027
        if (is_string($index->getName())) {
1028 42
            $indexName = $index->getName();
1029
        } else {
1030 42
            $indexName = $table->getName() . '_';
1031 2
            foreach ($index->getColumns() as $column) {
1032
                $indexName .= $column . '_';
1033 42
            }
1034
            $indexName .= 'index';
1035
        }
1036
        $def .= ' `' . $indexName . '`';
1037
1038
        return $def;
1039
    }
1040
1041
    /**
1042 8
     * {@inheritdoc}
1043
     */
1044 8
    public function getColumnTypes()
1045 2
    {
1046 2
        return array_merge(parent::getColumnTypes(), ['enum', 'json', 'jsonb']);
1047 6
    }
1048
1049 8
    /**
1050 3
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1051 3
     *
1052 6
     * @param \Phinx\Db\Table\ForeignKey $foreignKey
1053 6
     * @return string
1054 6
     */
1055 6 View Code Duplication
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1056 6
    {
1057
        $def = '';
1058 8
        if ($foreignKey->getConstraint()) {
1059 8
            $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...
1060
        } else {
1061
            $columnNames = [];
1062
            foreach ($foreignKey->getColumns() as $column) {
1063
                $columnNames[] = $this->quoteColumnName($column);
1064
            }
1065 47
            $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1066
            $refColumnNames = [];
1067 47
            foreach ($foreignKey->getReferencedColumns() as $column) {
1068
                $refColumnNames[] = $this->quoteColumnName($column);
1069
            }
1070
            $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1071
            if ($foreignKey->getOnDelete()) {
1072
                $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1073
            }
1074
            if ($foreignKey->getOnUpdate()) {
1075
                $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1076 5
            }
1077
        }
1078 5
1079 5
        return $def;
1080
    }
1081
1082 5
    /**
1083 5
     * {@inheritDoc}
1084 5
     *
1085 5
     */
1086 5
    public function getDecoratedConnection()
1087 5
    {
1088 5
        $options = $this->getOptions();
1089 5
        $options['quoteIdentifiers'] = true;
1090 5
        $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...
1091 5
1092 5
        if (!empty($options['name'])) {
1093 1
            $options['database'] = $options['name'];
1094 1
1095 5
            if (file_exists($options['name'] . '.sqlite3')) {
1096 1
                $options['database'] = $options['name'] . '.sqlite3';
1097 1
            }
1098
        }
1099 5
1100
        $driver = new SqliteDriver($options);
1101
        if (method_exists($driver, 'setConnection')) {
1102
            $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...
1103
        } else {
1104
            $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...
1105
        }
1106
1107
        return new Connection(['driver' => $driver] + $options);
1108
    }
1109
}
1110