Completed
Pull Request — master (#1284)
by
unknown
02:05 queued 22s
created

SQLiteAdapter::migrateTriggers()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 10
cts 10
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 12
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 * Copyright (c) 2015 Rob Morgan
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated * documentation files (the "Software"), to
10
 * deal in the Software without restriction, including without limitation the
11
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12
 * sell copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24
 * IN THE SOFTWARE.
25
 *
26
 * @package    Phinx
27
 * @subpackage Phinx\Db\Adapter
28
 */
29
namespace Phinx\Db\Adapter;
30
31
use Phinx\Db\Table;
32
use Phinx\Db\Table\Column;
33
use Phinx\Db\Table\ForeignKey;
34
use Phinx\Db\Table\Index;
35
36
/**
37
 * Phinx SQLite Adapter.
38
 *
39
 * @author Rob Morgan <[email protected]>
40
 * @author Richard McIntyre <[email protected]>
41
 */
42
class SQLiteAdapter extends PdoAdapter implements AdapterInterface
43
{
44
    protected $definitionsWithLimits = [
45
        'CHARACTER',
46
        'VARCHAR',
47
        'VARYING CHARACTER',
48
        'NCHAR',
49
        'NATIVE CHARACTER',
50
        'NVARCHAR'
51
    ];
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 42
    public function connect()
57
    {
58 42
        if ($this->connection === null) {
59 42
            if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers(), true)) {
60
                // @codeCoverageIgnoreStart
61
                throw new \RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
62
                // @codeCoverageIgnoreEnd
63
            }
64
65 42
            $db = null;
66 42
            $options = $this->getOptions();
67
68
            // if port is specified use it, otherwise use the MySQL default
69 42
            if (isset($options['memory'])) {
70
                $dsn = 'sqlite::memory:';
71
            } else {
72 42
                $dsn = 'sqlite:' . $options['name'];
73 42
                if (file_exists($options['name'] . '.sqlite3')) {
74 42
                    $dsn = 'sqlite:' . $options['name'] . '.sqlite3';
75 42
                }
76
            }
77
78
            try {
79 42
                $db = new \PDO($dsn);
80 42
            } catch (\PDOException $exception) {
81
                throw new \InvalidArgumentException(sprintf(
82
                    'There was a problem connecting to the database: %s',
83
                    $exception->getMessage()
84
                ));
85
            }
86
87 42
            $this->setConnection($db);
88 42
        }
89 42
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 48
    public function disconnect()
95
    {
96 48
        $this->connection = null;
97 48
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function hasTransactions()
103
    {
104
        return true;
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 1
    public function beginTransaction()
111
    {
112 1
        $this->execute('BEGIN TRANSACTION');
113 1
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function commitTransaction()
119
    {
120
        $this->execute('COMMIT');
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function rollbackTransaction()
127
    {
128
        $this->execute('ROLLBACK');
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 43
    public function quoteTableName($tableName)
135
    {
136 43
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 44
    public function quoteColumnName($columnName)
143
    {
144 44
        return '`' . str_replace('`', '``', $columnName) . '`';
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150 42
    public function hasTable($tableName)
151
    {
152 42
        $tables = [];
153 42
        $rows = $this->fetchAll(sprintf('SELECT name FROM sqlite_master WHERE type=\'table\' AND name=\'%s\'', $tableName));
154 42
        foreach ($rows as $row) {
155 12
            $tables[] = strtolower($row[0]);
156 42
        }
157
158 42
        return in_array(strtolower($tableName), $tables);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 42
    public function createTable(Table $table)
165
    {
166
        // Add the default primary key
167 42
        $columns = $table->getPendingColumns();
168 42
        $options = $table->getOptions();
169 42
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
170 35
            $column = new Column();
171 35
            $column->setName('id')
172 35
                   ->setType('integer')
173 35
                   ->setIdentity(true);
174
175 35
            array_unshift($columns, $column);
176 42
        } elseif (isset($options['id']) && is_string($options['id'])) {
177
            // Handle id => "field_name" to support AUTO_INCREMENT
178 1
            $column = new Column();
179 1
            $column->setName($options['id'])
180 1
                   ->setType('integer')
181 1
                   ->setIdentity(true);
182
183 1
            array_unshift($columns, $column);
184 1
        }
185
186
        $sql = 'CREATE TABLE ';
187 42
        $sql .= $this->quoteTableName($table->getName()) . ' (';
188 42 View Code Duplication
        foreach ($columns as $column) {
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...
189 42
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
190 42
        }
191 42
192
        // set the primary key(s)
193 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...
194 42
            $sql = rtrim($sql);
195 42
            $sql .= ' PRIMARY KEY (';
196 42
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
197 42
                $sql .= $this->quoteColumnName($options['primary_key']);
198 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...
199 42
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
200
            }
201
            $sql .= ')';
202 1
        } else {
203 1
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
204 1
        }
205
206 1
        // set the foreign keys
207 1
        $foreignKeys = $table->getForeignKeys();
208 1
        if (!empty($foreignKeys)) {
209 1
            foreach ($foreignKeys as $foreignKey) {
210 1
                $sql .= ', ' . $this->getForeignKeySqlDefinition($foreignKey);
211 1
            }
212 42
        }
213 42
214 37
        $sql = rtrim($sql) . ');';
215
        // execute the sql
216
        $this->execute($sql);
217
218 42
        foreach ($table->getIndexes() as $index) {
219 42
            $this->addIndex($table, $index);
220 1
        }
221 1
    }
222 1
223 1
    /**
224
     * {@inheritdoc}
225 42
     */
226
    public function renameTable($tableName, $newTableName)
227 42
    {
228
        $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $this->quoteTableName($tableName), $this->quoteTableName($newTableName)));
229 42
    }
230 6
231 42
    /**
232 42
     * {@inheritdoc}
233
     */
234
    public function dropTable($tableName)
235
    {
236
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
237 1
    }
238
239 1
    /**
240 1
     * {@inheritdoc}
241
     */
242
    public function truncateTable($tableName)
243
    {
244
        $sql = sprintf(
245 1
            'DELETE FROM %s',
246
            $this->quoteTableName($tableName)
247 1
        );
248 1
249
        $this->execute($sql);
250
    }
251
252
    /**
253 1
     * {@inheritdoc}
254
     */
255 1
    public function getColumns($tableName)
256 1
    {
257 1
        $columns = [];
258 1
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
259
260 1
        foreach ($rows as $columnInfo) {
261 1
            $column = new Column();
262
            $type = strtolower($columnInfo['type']);
263
            $column->setName($columnInfo['name'])
264
                   ->setNull($columnInfo['notnull'] !== '1')
265
                   ->setDefault($columnInfo['dflt_value']);
266 1
267
            $phinxType = $this->getPhinxType($type);
268 1
            $column->setType($phinxType['name'])
269 1
                   ->setLimit($phinxType['limit']);
270
271 1
            if ($columnInfo['pk'] == 1) {
272 1
                $column->setIdentity(true);
273 1
            }
274 1
275 1
            $columns[] = $column;
276 1
        }
277
278 1
        return $columns;
279 1
    }
280 1
281
    /**
282 1
     * {@inheritdoc}
283 1
     */
284 1 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...
285
    {
286 1
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
287 1
        foreach ($rows as $column) {
288
            if (strcasecmp($column['name'], $columnName) === 0) {
289 1
                return true;
290
            }
291
        }
292
293
        return false;
294
    }
295 8
296
    /**
297 8
     * {@inheritdoc}
298 8
     */
299 8 View Code Duplication
    public function addColumn(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...
300 7
    {
301
        $sql = sprintf(
302 8
            'ALTER TABLE %s ADD COLUMN %s %s',
303
            $this->quoteTableName($table->getName()),
304 8
            $this->quoteColumnName($column->getName()),
305
            $this->getColumnSqlDefinition($column)
306
        );
307
308
        $this->execute($sql);
309
    }
310 4
311
    /**
312 4
     * {@inheritdoc}
313 4
     */
314 4
    public function renameColumn($tableName, $columnName, $newColumnName)
315 4
    {
316 4
        $tmpTableName = 'tmp_' . $tableName;
317 4
318
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
319 4
320 4
        $sql = '';
321
        foreach ($rows as $table) {
322
            if ($table['tbl_name'] === $tableName) {
323
                $sql = $table['sql'];
324
            }
325 2
        }
326
327 2
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
328
        $selectColumns = [];
329 2
        $writeColumns = [];
330 View Code Duplication
        foreach ($columns as $column) {
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...
331 2
            $selectName = $column['name'];
332 2
            $writeName = ($selectName == $columnName)? $newColumnName : $selectName;
333 2
            $selectColumns[] = $this->quoteColumnName($selectName);
334 2
            $writeColumns[] = $this->quoteColumnName($writeName);
335 2
        }
336 2
337 View Code Duplication
        if (!in_array($this->quoteColumnName($columnName), $selectColumns)) {
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...
338 2
            throw new \InvalidArgumentException(sprintf(
339 2
                'The specified column doesn\'t exist: ' . $columnName
340 2
            ));
341 2
        }
342 2
343 2
        $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $tableName, $tmpTableName));
344 2
345 2
        $sql = str_replace(
346 2
            $this->quoteColumnName($columnName),
347
            $this->quoteColumnName($newColumnName),
348 2
            $sql
349 1
        );
350
        $this->execute($sql);
351 1
352
        $sql = sprintf(
353
            'INSERT INTO %s(%s) SELECT %s FROM %s',
354 1
            $tableName,
355
            implode(', ', $writeColumns),
356 1
            implode(', ', $selectColumns),
357 1
            $tmpTableName
358 1
        );
359
360 1
        $this->execute($sql);
361 1
        $this->migrateTriggers($tmpTableName, $tableName);
362
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName)));
363
    }
364 1
365 1
    /**
366 1
     * {@inheritdoc}
367 1
     */
368 1
    public function changeColumn($tableName, $columnName, Column $newColumn)
369
    {
370 1
        // TODO: DRY this up....
371
        $tmpTableName = 'tmp_' . $tableName;
372 1
373
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
374 1
375 1
        $sql = '';
376
        foreach ($rows as $table) {
377
            if ($table['tbl_name'] === $tableName) {
378
                $sql = $table['sql'];
379
            }
380 6
        }
381
382
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
383
        $selectColumns = [];
384 6
        $writeColumns = [];
385 View Code Duplication
        foreach ($columns as $column) {
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...
386 6
            $selectName = $column['name'];
387
            $writeName = ($selectName === $columnName)? $newColumn->getName() : $selectName;
388 6
            $selectColumns[] = $this->quoteColumnName($selectName);
389 6
            $writeColumns[] = $this->quoteColumnName($writeName);
390 6
        }
391 6
392 6 View Code Duplication
        if (!in_array($this->quoteColumnName($columnName), $selectColumns)) {
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...
393 6
            throw new \InvalidArgumentException(sprintf(
394
                'The specified column doesn\'t exist: ' . $columnName
395 6
            ));
396 6
        }
397 6
398 6
        $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $tableName, $tmpTableName));
399 6
400 6
        $sql = preg_replace(
401 6
            sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
402 6
            sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
403 6
            $sql,
404
            1
405 6
        );
406
407
        $this->execute($sql);
408
409
        $sql = sprintf(
410
            'INSERT INTO %s(%s) SELECT %s FROM %s',
411 6
            $tableName,
412
            implode(', ', $writeColumns),
413 6
            implode(', ', $selectColumns),
414 6
            $tmpTableName
415 6
        );
416 6
417
        $this->execute($sql);
418 6
        $this->migrateTriggers($tmpTableName, $tableName);
419
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName)));
420 6
    }
421
422 6
    /**
423 6
     * {@inheritdoc}
424 6
     */
425 6
    public function dropColumn($tableName, $columnName)
426 6
    {
427
        // TODO: DRY this up....
428 6
        $tmpTableName = 'tmp_' . $tableName;
429
430 6
        $rows = $this->fetchAll('select * from sqlite_master where `type` = \'table\'');
431 6
432 6
        $sql = '';
433
        foreach ($rows as $table) {
434
            if ($table['tbl_name'] === $tableName) {
435
                $sql = $table['sql'];
436
            }
437 2
        }
438
439
        $rows = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
440 2
        $columns = [];
441
        $columnType = null;
442 2
        foreach ($rows as $row) {
443
            if ($row['name'] !== $columnName) {
444 2
                $columns[] = $row['name'];
445 2
            } else {
446 2
                $found = true;
447 2
                $columnType = $row['type'];
448 2
            }
449 2
        }
450
451 2
        if (!isset($found)) {
452 2
            throw new \InvalidArgumentException(sprintf(
453 2
                'The specified column doesn\'t exist: ' . $columnName
454 2
            ));
455 2
        }
456 2
457 2
        $this->execute(sprintf('ALTER TABLE %s RENAME TO %s', $tableName, $tmpTableName));
458 2
459 2
        $sql = preg_replace(
460
            sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($columnType)),
461 2
            "",
462
            $sql
463 2
        );
464
465
        if (substr($sql, -2) === ', ') {
466
            $sql = substr($sql, 0, -2) . ')';
467
        }
468
469 2
        $this->execute($sql);
470
471 2
        $sql = sprintf(
472 2
            'INSERT INTO %s(%s) SELECT %s FROM %s',
473 2
            $tableName,
474
            implode(', ', $columns),
475 2
            implode(', ', $columns),
476
            $tmpTableName
477 2
        );
478 2
479 2
        $this->execute($sql);
480
        $this->migrateTriggers($tmpTableName, $tableName);
481 2
        $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tmpTableName)));
482
    }
483 2
484 2
    /**
485 2
     * Moves the triggers from one temporary table to another. Useful when the
486 2
     * operation you are about to perform requires DROP'ing the table.
487 2
     *
488
     * @param  string $oldTableName
489 2
     * @param  string $newTableName
490
     * @return void
491 2
     */
492 2
    protected function migrateTriggers($oldTableName, $newTableName)
493 2
    {
494
        $rows = $this->fetchAll(sprintf(
495
            'select * from sqlite_master where type = "trigger" and tbl_name = "%s";',
496
            $oldTableName
497
        ));
498
499
        foreach ($rows as $row) {
500
            $newTrigger = preg_replace(
501 9
                sprintf('/%s/i', preg_quote($oldTableName)),
502
                $newTableName,
503 9
                $row['sql'],
504 9
                1
505
            );
506 9
507 9
            $this->execute(sprintf('DROP TRIGGER %s', $this->quoteTableName($row['name'])));
508 9
            $this->execute($newTrigger);
509 9
        }
510 9
    }
511 9
512 9
    /**
513 9
     * Get an array of indexes from a particular table.
514 9
     *
515 9
     * @param string $tableName Table Name
516
     * @return array
517
     */
518
    protected function getIndexes($tableName)
519
    {
520
        $indexes = [];
521 9
        $rows = $this->fetchAll(sprintf('pragma index_list(%s)', $tableName));
522
523 9
        foreach ($rows as $row) {
524 4
            $indexData = $this->fetchAll(sprintf('pragma index_info(%s)', $row['name']));
525 4
            if (!isset($indexes[$tableName])) {
526
                $indexes[$tableName] = ['index' => $row['name'], 'columns' => []];
527 9
            }
528 9
            foreach ($indexData as $indexItem) {
529
                $indexes[$tableName]['columns'][] = strtolower($indexItem['name']);
530 9
            }
531 9
        }
532 9
533 9
        return $indexes;
534
    }
535 8
536
    /**
537 8
     * {@inheritdoc}
538
     */
539 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...
540
    {
541
        if (is_string($columns)) {
542
            $columns = [$columns]; // str to array
543 1
        }
544
545 1
        $columns = array_map('strtolower', $columns);
546
        $indexes = $this->getIndexes($tableName);
547 1
548 1
        foreach ($indexes as $index) {
549 1
            $a = array_diff($columns, $index['columns']);
550
            if (empty($a)) {
551
                return true;
552
            }
553
        }
554
555
        return false;
556
    }
557
558
    /**
559 8
     * {@inheritdoc}
560
     */
561 8 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...
562 8
    {
563 8
        $indexes = $this->getIndexes($tableName);
564 8
565 8
        foreach ($indexes as $index) {
566 8
            if ($indexName === $index['index']) {
567 8
                return true;
568 8
            }
569 8
        }
570 8
571
        return false;
572 8
    }
573 8
574 8
    /**
575
     * {@inheritdoc}
576
     */
577
    public function addIndex(Table $table, Index $index)
578
    {
579 1
        $indexColumnArray = [];
580
        foreach ($index->getColumns() as $column) {
581 1
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
582 1
        }
583 1
        $indexColumns = implode(',', $indexColumnArray);
584
        $this->execute(
585 1
            sprintf(
586 1
                'CREATE %s ON %s (%s)',
587
                $this->getIndexSqlDefinition($table, $index),
588 1
                $this->quoteTableName($table->getName()),
589 1
                $indexColumns
590 1
            )
591 1
        );
592 1
    }
593 1
594 1
    /**
595 1
     * {@inheritdoc}
596 1
     */
597 1
    public function dropIndex($tableName, $columns)
598
    {
599
        if (is_string($columns)) {
600
            $columns = [$columns]; // str to array
601
        }
602
603
        $indexes = $this->getIndexes($tableName);
604
        $columns = array_map('strtolower', $columns);
605 1
606
        foreach ($indexes as $index) {
607 1
            $a = array_diff($columns, $index['columns']);
608
            if (empty($a)) {
609 1
                $this->execute(
610 1
                    sprintf(
611 1
                        'DROP INDEX %s',
612 1
                        $this->quoteColumnName($index['index'])
613 1
                    )
614 1
                );
615 1
616 1
                return;
617 1
            }
618
        }
619
    }
620
621
    /**
622
     * {@inheritdoc}
623
     */
624 View Code Duplication
    public function dropIndexByName($tableName, $indexName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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