Completed
Pull Request — master (#1842)
by
unknown
01:44
created

SQLiteAdapter::getDeclaringIndexSql()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 10
cts 10
cp 1
rs 9.8333
c 0
b 0
f 0
cc 4
nc 3
nop 2
crap 4
1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Adapter;
9
10
use BadMethodCallException;
11
use Cake\Database\Connection;
12
use Cake\Database\Driver\Sqlite as SqliteDriver;
13
use InvalidArgumentException;
14
use PDO;
15
use PDOException;
16
use Phinx\Db\Table\Column;
17
use Phinx\Db\Table\ForeignKey;
18
use Phinx\Db\Table\Index;
19
use Phinx\Db\Table\Table;
20
use Phinx\Db\Util\AlterInstructions;
21
use Phinx\Util\Expression;
22
use Phinx\Util\Literal;
23
use RuntimeException;
24
25
/**
26
 * Phinx SQLite Adapter.
27
 *
28
 * @author Rob Morgan <[email protected]>
29
 * @author Richard McIntyre <[email protected]>
30
 */
31
class SQLiteAdapter extends PdoAdapter
32
{
33
    public const MEMORY = ':memory:';
34
35
    /**
36
     * List of supported Phinx column types with their SQL equivalents
37
     * some types have an affinity appended to ensure they do not receive NUMERIC affinity
38
     *
39
     * @var string[]
40
     */
41
    protected static $supportedColumnTypes = [
42
        self::PHINX_TYPE_BIG_INTEGER => 'biginteger',
43
        self::PHINX_TYPE_BINARY => 'binary_blob',
44
        self::PHINX_TYPE_BINARYUUID => 'binary_blob',
45
        self::PHINX_TYPE_BLOB => 'blob',
46
        self::PHINX_TYPE_BOOLEAN => 'boolean_integer',
47
        self::PHINX_TYPE_CHAR => 'char',
48
        self::PHINX_TYPE_DATE => 'date_text',
49
        self::PHINX_TYPE_DATETIME => 'datetime_text',
50
        self::PHINX_TYPE_DOUBLE => 'double',
51
        self::PHINX_TYPE_FLOAT => 'float',
52
        self::PHINX_TYPE_INTEGER => 'integer',
53
        self::PHINX_TYPE_JSON => 'json_text',
54
        self::PHINX_TYPE_JSONB => 'jsonb_text',
55
        self::PHINX_TYPE_SMALL_INTEGER => 'smallinteger',
56 42
        self::PHINX_TYPE_STRING => 'varchar',
57
        self::PHINX_TYPE_TEXT => 'text',
58 42
        self::PHINX_TYPE_TIME => 'time_text',
59 42
        self::PHINX_TYPE_TIMESTAMP => 'timestamp_text',
60
        self::PHINX_TYPE_TINY_INTEGER => 'tinyinteger',
61
        self::PHINX_TYPE_UUID => 'uuid_text',
62
        self::PHINX_TYPE_VARBINARY => 'varbinary_blob',
63
    ];
64
65 42
    /**
66 42
     * List of aliases of supported column types
67
     *
68
     * @var string[]
69 42
     */
70
    protected static $supportedColumnTypeAliases = [
71
        'varchar' => self::PHINX_TYPE_STRING,
72 42
        'tinyint' => self::PHINX_TYPE_TINY_INTEGER,
73 42
        'tinyinteger' => self::PHINX_TYPE_TINY_INTEGER,
74 42
        'smallint' => self::PHINX_TYPE_SMALL_INTEGER,
75 42
        'int' => self::PHINX_TYPE_INTEGER,
76
        'mediumint' => self::PHINX_TYPE_INTEGER,
77
        'mediuminteger' => self::PHINX_TYPE_INTEGER,
78
        'bigint' => self::PHINX_TYPE_BIG_INTEGER,
79 42
        'tinytext' => self::PHINX_TYPE_TEXT,
80 42
        'mediumtext' => self::PHINX_TYPE_TEXT,
81
        'longtext' => self::PHINX_TYPE_TEXT,
82
        'tinyblob' => self::PHINX_TYPE_BLOB,
83
        'mediumblob' => self::PHINX_TYPE_BLOB,
84
        'longblob' => self::PHINX_TYPE_BLOB,
85
        'real' => self::PHINX_TYPE_FLOAT,
86
    ];
87 42
88 42
    /**
89 42
     * List of known but unsupported Phinx column types
90
     *
91
     * @var string[]
92
     */
93
    protected static $unsupportedColumnTypes = [
94 48
        self::PHINX_TYPE_BIT,
95
        self::PHINX_TYPE_CIDR,
96 48
        self::PHINX_TYPE_DECIMAL,
97 48
        self::PHINX_TYPE_ENUM,
98
        self::PHINX_TYPE_FILESTREAM,
99
        self::PHINX_TYPE_GEOMETRY,
100
        self::PHINX_TYPE_INET,
101
        self::PHINX_TYPE_INTERVAL,
102
        self::PHINX_TYPE_LINESTRING,
103
        self::PHINX_TYPE_MACADDR,
104
        self::PHINX_TYPE_POINT,
105
        self::PHINX_TYPE_POLYGON,
106
        self::PHINX_TYPE_SET,
107
    ];
108
109
    /**
110 1
     * @var string[]
111
     */
112 1
    protected $definitionsWithLimits = [
113 1
        'CHAR',
114
        'CHARACTER',
115
        'VARCHAR',
116
        'VARYING CHARACTER',
117
        'NCHAR',
118
        'NATIVE CHARACTER',
119
        'NVARCHAR',
120
    ];
121
122
    protected $suffix = '.sqlite3';
123
124
    /**
125
     * Indicates whether the database library version is at least the specified version
126
     *
127
     * @param string $ver The version to check against e.g. '3.28.0'
128
     *
129
     * @return bool
130
     */
131
    public function databaseVersionAtLeast($ver)
132
    {
133
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
134 43
135
        return version_compare($actual, $ver, '>=');
136 43
    }
137
138
    /**
139
     * {@inheritDoc}
140
     *
141
     * @throws \RuntimeException
142 44
     * @throws \InvalidArgumentException
143
     *
144 44
     * @return void
145
     */
146
    public function connect()
147
    {
148
        if ($this->connection === null) {
149
            if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) {
150 42
                // @codeCoverageIgnoreStart
151
                throw new RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
152 42
                // @codeCoverageIgnoreEnd
153 42
            }
154 42
155 12
            $options = $this->getOptions();
156 42
157
            // use a memory database if the option was specified
158 42
            if (!empty($options['memory']) || $options['name'] === static::MEMORY) {
159
                $dsn = 'sqlite:' . static::MEMORY;
160
            } else {
161
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
162
            }
163
164 42
            $driverOptions = [];
165
166
            // use custom data fetch mode
167 42
            if (!empty($options['fetch_mode'])) {
168 42
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
169 42
            }
170 35
171 35
            $db = $this->createPdoConnection($dsn, null, null, $driverOptions);
172 35
173 35
            $this->setConnection($db);
174
        }
175 35
    }
176 42
177
    /**
178 1
     * @inheritDoc
179 1
     */
180 1
    public function setOptions(array $options)
181 1
    {
182
        parent::setOptions($options);
183 1
184 1
        if (isset($options['suffix'])) {
185
            $this->suffix = $options['suffix'];
186
        }
187 42
        //don't "fix" the file extension if it is blank, some people
188 42
        //might want a SQLITE db file with absolutely no extension.
189 42
        if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) {
190 42
            $this->suffix = '.' . $this->suffix;
191 42
        }
192
193
        return $this;
194 42
    }
195 42
196 42
    /**
197 42
     * @inheritDoc
198 42
     */
199 42
    public function disconnect()
200
    {
201
        $this->connection = null;
202 1
    }
203 1
204 1
    /**
205
     * @inheritDoc
206 1
     */
207 1
    public function hasTransactions()
208 1
    {
209 1
        return true;
210 1
    }
211 1
212 42
    /**
213 42
     * @inheritDoc
214 37
     */
215
    public function beginTransaction()
216
    {
217
        $this->getConnection()->beginTransaction();
218 42
    }
219 42
220 1
    /**
221 1
     * @inheritDoc
222 1
     */
223 1
    public function commitTransaction()
224
    {
225 42
        $this->getConnection()->commit();
226
    }
227 42
228
    /**
229 42
     * @inheritDoc
230 6
     */
231 42
    public function rollbackTransaction()
232 42
    {
233
        $this->getConnection()->rollBack();
234
    }
235
236
    /**
237 1
     * @inheritDoc
238
     */
239 1
    public function quoteTableName($tableName)
240 1
    {
241
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
242
    }
243
244
    /**
245 1
     * @inheritDoc
246
     */
247 1
    public function quoteColumnName($columnName)
248 1
    {
249
        return '`' . str_replace('`', '``', $columnName) . '`';
250
    }
251
252
    /**
253 1
     * @param string $tableName Table name
254
     * @param bool $quoted Whether to return the schema name and table name escaped and quoted. If quoted, the schema (if any) will also be appended with a dot
255 1
     *
256 1
     * @return array
257 1
     */
258 1
    protected function getSchemaName($tableName, $quoted = false)
259
    {
260 1
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
261 1
            $table = $match[1];
262
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
263
            $result = ['schema' => $schema, 'table' => $table];
264
        } else {
265
            $result = ['schema' => '', 'table' => $tableName];
266 1
        }
267
268 1
        if ($quoted) {
269 1
            $result['schema'] = $result['schema'] !== '' ? $this->quoteColumnName($result['schema']) . '.' : '';
270
            $result['table'] = $this->quoteColumnName($result['table']);
271 1
        }
272 1
273 1
        return $result;
274 1
    }
275 1
276 1
    /**
277
     * Retrieves information about a given table from one of the SQLite pragmas
278 1
     *
279 1
     * @param string $tableName The table to query
280 1
     * @param string $pragma The pragma to query
281
     *
282 1
     * @return array
283 1
     */
284 1
    protected function getTableInfo($tableName, $pragma = 'table_info')
285
    {
286 1
        $info = $this->getSchemaName($tableName, true);
287 1
288
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
289 1
    }
290
291
    /**
292
     * Searches through all available schemata to find a table and returns an array
293
     * containing the bare schema name and whether the table exists at all.
294
     * If no schema was specified and the table does not exist the "main" schema is returned
295 8
     *
296
     * @param string $tableName The name of the table to find
297 8
     *
298 8
     * @return array
299 8
     */
300 7
    protected function resolveTable($tableName)
301
    {
302 8
        $info = $this->getSchemaName($tableName);
303
        if ($info['schema'] === '') {
304 8
            // if no schema is specified we search all schemata
305
            $rows = $this->fetchAll('PRAGMA database_list;');
306
            // the temp schema is always first to be searched
307
            $schemata = ['temp'];
308
            foreach ($rows as $row) {
309
                if (strtolower($row['name']) !== 'temp') {
310 4
                    $schemata[] = $row['name'];
311
                }
312 4
            }
313 4
            $defaultSchema = 'main';
314 4
        } else {
315 4
            // otherwise we search just the specified schema
316 4
            $schemata = (array)$info['schema'];
317 4
            $defaultSchema = $info['schema'];
318
        }
319 4
320 4
        $table = strtolower($info['table']);
321
        foreach ($schemata as $schema) {
322
            if (strtolower($schema) === 'temp') {
323
                $master = 'sqlite_temp_master';
324
            } else {
325 2
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
326
            }
327 2
            try {
328
                $rows = $this->fetchAll(sprintf("SELECT name FROM %s WHERE type='table' AND lower(name) = %s", $master, $this->quoteString($table)));
329 2
            } catch (PDOException $e) {
330
                // an exception can occur if the schema part of the table refers to a database which is not attached
331 2
                break;
332 2
            }
333 2
334 2
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
335 2
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
336 2
            foreach ($rows as $row) {
337
                if (strtolower($row['name']) === $table) {
338 2
                    return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
339 2
                }
340 2
            }
341 2
        }
342 2
343 2
        return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false];
344 2
    }
345 2
346 2
    /**
347
     * @inheritDoc
348 2
     */
349 1
    public function hasTable($tableName)
350
    {
351 1
        return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists'];
352
    }
353
354 1
    /**
355
     * @inheritDoc
356 1
     */
357 1
    public function createTable(Table $table, array $columns = [], array $indexes = [])
358 1
    {
359
        // Add the default primary key
360 1
        $options = $table->getOptions();
361 1
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
362
            $options['id'] = 'id';
363
        }
364 1
365 1
        if (isset($options['id']) && is_string($options['id'])) {
366 1
            // Handle id => "field_name" to support AUTO_INCREMENT
367 1
            $column = new Column();
368 1
            $column->setName($options['id'])
369
                   ->setType('integer')
370 1
                   ->setIdentity(true);
371
372 1
            array_unshift($columns, $column);
373
        }
374 1
375 1
        $sql = 'CREATE TABLE ';
376
        $sql .= $this->quoteTableName($table->getName()) . ' (';
377
        foreach ($columns as $column) {
378
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
379
380 6
            if (isset($options['primary_key']) && $column->getIdentity()) {
381
                //remove column from the primary key array as it is already defined as an autoincrement
382
                //primary id
383
                $identityColumnIndex = array_search($column->getName(), $options['primary_key'], true);
384 6
                if ($identityColumnIndex !== false) {
385
                    unset($options['primary_key'][$identityColumnIndex]);
386 6
387
                    if (empty($options['primary_key'])) {
388 6
                        //The last primary key has been removed
389 6
                        unset($options['primary_key']);
390 6
                    }
391 6
                }
392 6
            }
393 6
        }
394
395 6
        // set the primary key(s)
396 6
        if (isset($options['primary_key'])) {
397 6
            $sql = rtrim($sql);
398 6
            $sql .= ' PRIMARY KEY (';
399 6
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
400 6
                $sql .= $this->quoteColumnName($options['primary_key']);
401 6
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
402 6
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
403 6
            }
404
            $sql .= ')';
405 6
        } else {
406
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
407
        }
408
409
        $sql = rtrim($sql) . ');';
410
        // execute the sql
411 6
        $this->execute($sql);
412
413 6
        foreach ($indexes as $index) {
414 6
            $this->addIndex($table, $index);
415 6
        }
416 6
417
        $this->addCreatedTable($table->getName());
418 6
    }
419
420 6
    /**
421
     * {@inheritDoc}
422 6
     *
423 6
     * @throws \InvalidArgumentException
424 6
     */
425 6
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
426 6
    {
427
        $instructions = new AlterInstructions();
428 6
429
        // Drop the existing primary key
430 6
        $primaryKey = $this->getPrimaryKey($table->getName());
431 6
        if (!empty($primaryKey)) {
432 6
            $instructions->merge(
433
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
434
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
435
            );
436
        }
437 2
438
        // Add the primary key(s)
439
        if (!empty($newColumns)) {
440 2
            if (!is_string($newColumns)) {
441
                throw new InvalidArgumentException(sprintf(
442 2
                    'Invalid value for primary key: %s',
443
                    json_encode($newColumns)
444 2
                ));
445 2
            }
446 2
447 2
            $instructions->merge(
448 2
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
449 2
            );
450
        }
451 2
452 2
        return $instructions;
453 2
    }
454 2
455 2
    /**
456 2
     * @inheritDoc
457 2
     *
458 2
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
459 2
     *
460
     * @throws \BadMethodCallException
461 2
     */
462
    protected function getChangeCommentInstructions(Table $table, $newComment)
463 2
    {
464
        throw new BadMethodCallException('SQLite does not have table comments');
465
    }
466
467
    /**
468
     * @inheritDoc
469 2
     */
470
    protected function getRenameTableInstructions($tableName, $newTableName)
471 2
    {
472 2
        $this->updateCreatedTableName($tableName, $newTableName);
473 2
        $sql = sprintf(
474
            'ALTER TABLE %s RENAME TO %s',
475 2
            $this->quoteTableName($tableName),
476
            $this->quoteTableName($newTableName)
477 2
        );
478 2
479 2
        return new AlterInstructions([], [$sql]);
480
    }
481 2
482
    /**
483 2
     * @inheritDoc
484 2
     */
485 2
    protected function getDropTableInstructions($tableName)
486 2
    {
487 2
        $this->removeCreatedTable($tableName);
488
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
489 2
490
        return new AlterInstructions([], [$sql]);
491 2
    }
492 2
493 2
    /**
494
     * @inheritDoc
495
     */
496
    public function truncateTable($tableName)
497
    {
498
        $info = $this->resolveTable($tableName);
499
        // first try deleting the rows
500
        $this->execute(sprintf(
501 9
            'DELETE FROM %s.%s',
502
            $this->quoteColumnName($info['schema']),
503 9
            $this->quoteColumnName($info['table'])
504 9
        ));
505
506 9
        // assuming no error occurred, reset the autoincrement (if any)
507 9
        if ($this->hasTable($info['schema'] . '.sqlite_sequence')) {
508 9
            $this->execute(sprintf(
509 9
                'DELETE FROM %s.%s where name  = %s',
510 9
                $this->quoteColumnName($info['schema']),
511 9
                'sqlite_sequence',
512 9
                $this->quoteString($info['table'])
513 9
            ));
514 9
        }
515 9
    }
516
517
    /**
518
     * Parses a default-value expression to yield either a Literal representing
519
     * a string value, a string representing an expression, or some other scalar
520
     *
521 9
     * @param mixed $v The default-value expression to interpret
522
     * @param string $t The Phinx type of the column
523 9
     *
524 4
     * @return mixed
525 4
     */
526
    protected function parseDefaultValue($v, $t)
527 9
    {
528 9
        if ($v === null) {
529
            return null;
530 9
        }
531 9
532 9
        // split the input into tokens
533 9
        $trimChars = " \t\n\r\0\x0B";
534
        $pattern = <<<PCRE_PATTERN
535 8
            /
536
                '(?:[^']|'')*'|                 # String literal
537 8
                "(?:[^"]|"")*"|                 # Standard identifier
538
                `(?:[^`]|``)*`|                 # MySQL identifier
539
                \[[^\]]*\]|                     # SQL Server identifier
540
                --[^\r\n]*|                     # Single-line comment
541
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
542
                [^\/\-]+|                       # Non-special characters
543 1
                .                               # Any other single character
544
            /sx
545 1
PCRE_PATTERN;
546
        preg_match_all($pattern, $v, $matches);
547 1
        // strip out any comment tokens
548 1
        $matches = array_map(function ($v) {
549 1
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
550
        }, $matches[0]);
551
        // reconstitute the string, trimming whitespace as well as parentheses
552
        $vClean = trim(implode('', $matches));
553
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
554
555
        // match the string against one of several patterns
556
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
557
            // magic date or time
558
            return strtoupper($vBare);
559 8
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
560
            // string literal
561 8
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
562 8
563 8
            return Literal::from($str);
564 8
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
565 8
            $int = (int)$vBare;
566 8
            // integer literal
567 8
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int === 0 || $int === 1)) {
568 8
                return (bool)$int;
569 8
            } else {
570 8
                return $int;
571
            }
572 8
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
573 8
            // float literal
574 8
            return (float)$vBare;
575
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
576
            // hexadecimal literal
577
            return hexdec(substr($vBare, 2));
578
        } elseif (preg_match('/^null$/i', $vBare)) {
579 1
            // null literal
580
            return null;
581 1
        } elseif (preg_match('/^true|false$/i', $vBare)) {
582 1
            // boolean literal
583 1
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
584
        } else {
585 1
            // any other expression: return the expression with parentheses, but without comments
586 1
            return Expression::from($vClean);
587
        }
588 1
    }
589 1
590 1
    /**
591 1
     * Returns the name of the specified table's identity column, or null if the table has no identity
592 1
     *
593 1
     * The process of finding an identity column is somewhat convoluted as SQLite has no direct way of querying whether a given column is an alias for the table's row ID
594 1
     *
595 1
     * @param string $tableName The name of the table
596 1
     *
597 1
     * @return string|null
598
     */
599
    protected function resolveIdentity($tableName)
600
    {
601
        $result = null;
602
        // make sure the table has only one primary key column which is of type integer
603
        foreach ($this->getTableInfo($tableName) as $col) {
604
            $type = strtolower($col['type']);
605 1
            if ($col['pk'] > 1) {
606
                // the table has a composite primary key
607 1
                return null;
608
            } elseif ($col['pk'] == 0) {
609 1
                // the column is not a primary key column and is thus not relevant
610 1
                continue;
611 1
            } elseif ($type !== 'integer') {
612 1
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
613 1
                return null;
614 1
            } else {
615 1
                // the column is a candidate for a row ID alias
616 1
                $result = $col['name'];
617 1
            }
618
        }
619
        // if there is no suitable PK column, stop now
620
        if ($result === null) {
621
            return null;
622
        }
623
        // make sure the table does not have a PK-origin autoindex
624
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
625 5
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
626
            if ($idx['origin'] === 'pk') {
627 5
                return null;
628
            }
629
        }
630 5
631
        return $result;
632 5
    }
633 5
634 5
    /**
635
     * @inheritDoc
636 1
     */
637
    public function getColumns($tableName)
638
    {
639
        $columns = [];
640
641
        $rows = $this->getTableInfo($tableName);
642
        $identity = $this->resolveIdentity($tableName);
643
644
        foreach ($rows as $columnInfo) {
645 5
            $column = new Column();
646
            $type = $this->getPhinxType($columnInfo['type']);
647 5
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
0 ignored issues
show
Bug introduced by
It seems like $type['name'] can also be of type null or object<Phinx\Util\Literal>; however, Phinx\Db\Adapter\SQLiteA...er::parseDefaultValue() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
648 5
649
            $column->setName($columnInfo['name'])
650
                   ->setNull($columnInfo['notnull'] !== '1')
651
                   ->setDefault($default)
652
                   ->setType($type['name'])
0 ignored issues
show
Bug introduced by
It seems like $type['name'] can also be of type null; however, Phinx\Db\Table\Column::setType() does only seem to accept string|object<Phinx\Util\Literal>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
653
                   ->setLimit($type['limit'])
654
                   ->setScale($type['scale'])
655
                   ->setIdentity($columnInfo['name'] === $identity);
656
657
            $columns[] = $column;
658
        }
659
660
        return $columns;
661 5
    }
662
663 5
    /**
664 5
     * @inheritDoc
665 5
     */
666 5
    public function hasColumn($tableName, $columnName)
667 5
    {
668 5
        $rows = $this->getTableInfo($tableName);
669 5
        foreach ($rows as $column) {
670 5
            if (strcasecmp($column['name'], $columnName) === 0) {
671 5
                return true;
672 5
            }
673 5
        }
674
675
        return false;
676
    }
677
678
    /**
679 4
     * @inheritDoc
680
     */
681
    protected function getAddColumnInstructions(Table $table, Column $column)
682 4
    {
683
        $tableName = $table->getName();
684 4
685 4
        $instructions = $this->beginAlterByCopyTable($tableName);
686
687 4
        $instructions->addPostStep(function ($state) use ($column) {
688 4
            $sql = preg_replace(
689 4
                "/\)$/",
690 4
                sprintf(', %s %s$1)', $this->quoteColumnName($column->getName()), $this->getColumnSqlDefinition($column)),
691 4
                $state['createSQL'],
692 4
                1
693
            );
694 4
            $this->execute($sql);
695 4
696 4
            return $state;
697 4
        });
698 4
699
        $instructions->addPostStep(function ($state) use ($tableName) {
700 4
            $newState = $this->calculateNewTableColumns($tableName, false, false);
701
702 4
            return $newState + $state;
703 4
        });
704
705 4
        return $this->copyAndDropTmpTable($instructions, $tableName);
706 4
    }
707 4
708 4
    /**
709 4
     * Returns the original CREATE statement for the give table
710 4
     *
711 4
     * @param string $tableName The table name to get the create statement for
712
     *
713 4
     * @return string
714 4
     */
715 4
    protected function getDeclaringSql($tableName)
716
    {
717
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'table'");
718
719
        $sql = '';
720 1
        foreach ($rows as $table) {
721
            if ($table['tbl_name'] === $tableName) {
722
                $sql = $table['sql'];
723 1
            }
724
        }
725
726
        return $sql;
727 1
    }
728
729 1
    /**
730
     * Returns the original CREATE statement for the give index
731 1
     *
732 1
     * @param string $tableName The table name to get the create statement for
733 1
     * @return string
734 1
     */
735 1
    protected function getDeclaringIndexSql($tableName, $indexName)
736 1
    {
737
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'index'");
738 1
739 1
        $sql = '';
740 1
        foreach ($rows as $table) {
741 1
            if ($table['tbl_name'] === $tableName && $table['name'] === $indexName) {
742 1
                $sql = $table['sql'] . '; ';
743 1
            }
744 1
        }
745
746 1
        return $sql;
747
    }
748 1
749
    /**
750
     * Copies all the data from a tmp table to another table
751
     *
752
     * @param string $tableName The table name to copy the data to
753
     * @param string $tmpTableName The tmp table name where the data is stored
754 1
     * @param string[] $writeColumns The list of columns in the target table
755
     * @param string[] $selectColumns The list of columns in the tmp table
756 1
     *
757 1
     * @return void
758 1
     */
759 1
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
760 1
    {
761 1
        $sql = sprintf(
762 1
            'INSERT INTO %s(%s) SELECT %s FROM %s',
763
            $this->quoteTableName($tableName),
764 1
            implode(', ', $writeColumns),
765
            implode(', ', $selectColumns),
766 1
            $this->quoteTableName($tmpTableName)
767 1
        );
768 1
        $this->execute($sql);
769 1
    }
770 1
771
    /**
772 1
     * Modifies the passed instructions to copy all data from the table into
773
     * the provided tmp table and then drops the table and rename tmp table.
774 1
     *
775 1
     * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
776 1
     * @param string $tableName The table name to copy the data to
777
     *
778
     * @return \Phinx\Db\Util\AlterInstructions
779
     */
780
    protected function copyAndDropTmpTable($instructions, $tableName)
781
    {
782
        $instructions->addPostStep(function ($state) use ($tableName) {
783
            $this->copyDataToNewTable(
784
                $state['tmpTableName'],
785
                $tableName,
786
                $state['writeColumns'],
787
                $state['selectColumns']
788
            );
789
790
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
791
            $this->execute(sprintf(
792
                'ALTER TABLE %s RENAME TO %s',
793
                $this->quoteTableName($state['tmpTableName']),
794
                $this->quoteTableName($tableName)
795
            ));
796
797
            return $state;
798
        });
799
800
        return $instructions;
801
    }
802
803
    /**
804
     * Returns the columns and type to use when copying a table to another in the process
805
     * of altering a table
806
     *
807
     * @param string $tableName The table to modify
808
     * @param string|false $columnName The column name that is about to change
809
     * @param string|false $newColumnName Optionally the new name for the column
810 43
     *
811
     * @throws \InvalidArgumentException
812
     *
813 43
     * @return array
814 42
     */
815
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
816 43
    {
817
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
818
        $selectColumns = [];
819 43
        $writeColumns = [];
820 1
        $columnType = null;
821
        $found = false;
822 43
823 38
        foreach ($columns as $column) {
824
            $selectName = $column['name'];
825 43
            $writeName = $selectName;
826 42
827
            if ($selectName === $columnName) {
828 43
                $writeName = $newColumnName;
829 2
                $found = true;
830
                $columnType = $column['type'];
831 43
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
832 1
            }
833
834 43
            $selectColumns[] = $selectName;
835 1
            $writeColumns[] = $writeName;
836
        }
837 43
838 42
        $selectColumns = array_filter($selectColumns, 'strlen');
839
        $writeColumns = array_filter($writeColumns, 'strlen');
840 43
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
841 1
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
842
843 43
        if ($columnName && !$found) {
844 1
            throw new InvalidArgumentException(sprintf(
845
                'The specified column doesn\'t exist: ' . $columnName
846 43
            ));
847 43
        }
848 1
849
        return compact('writeColumns', 'selectColumns', 'columnType');
850 43
    }
851 42
852
    /**
853 5
     * Returns the initial instructions to alter a table using the
854
     * create-copy-drop strategy
855 5
     *
856 4
     * @param string $tableName The table to modify
857
     *
858
     * @return \Phinx\Db\Util\AlterInstructions
859
     */
860 1
    protected function beginAlterByCopyTable($tableName)
861 1
    {
862
        $instructions = new AlterInstructions();
863
        $instructions->addPostStep(function ($state) use ($tableName) {
864 1
            $tmpTableName = "tmp_{$tableName}";
865
            $createSQL = $this->getDeclaringSql($tableName);
866
            // Table name in SQLite can be hilarious inside declaring SQL:
867 1
            // - tableName
868
            // - `tableName`
869 1
            // - "tableName"
870 1
            // - [this is a valid table name too!]
871 1
            // - etc.
872
            // Just remove all characters before first "(" and build them again
873
            $createSQL = preg_replace(
874
                "/^CREATE TABLE .* \(/Ui",
875
                '',
876
                $createSQL
877
            );
878
            $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
879
880 3
            return compact('createSQL', 'tmpTableName') + $state;
881
        });
882 3
883 1
        return $instructions;
884
    }
885 2
886 2
    /**
887 2
     * @inheritDoc
888 2
     */
889 1
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
890 1
    {
891 2
        $instructions = $this->beginAlterByCopyTable($tableName);
892
893
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
894 2
            $sql = str_replace(
895 2
                $this->quoteColumnName($columnName),
896 1
                $this->quoteColumnName($newColumnName),
897 1
                $state['createSQL']
898
            );
899
            $this->execute($sql);
900 1
901 2
            return $state;
902
        });
903
904
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
905
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
906
907
            return $newState + $state;
908
        });
909
910 2
        return $this->copyAndDropTmpTable($instructions, $tableName);
911
    }
912
913
    /**
914
     * @inheritDoc
915
     */
916 2
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
917 1
    {
918
        $instructions = $this->beginAlterByCopyTable($tableName);
919
920 1
        $newColumnName = $newColumn->getName();
921 1
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
922 2
            $sql = preg_replace(
923 1
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
924 1
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
925 2
                $state['createSQL'],
926 2
                1
927
            );
928
            $this->execute($sql);
929
930
            return $state;
931
        });
932
933 2
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
934
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
935
936 1
            return $newState + $state;
937 1
        });
938
939 1
        return $this->copyAndDropTmpTable($instructions, $tableName);
940
    }
941
942
    /**
943
     * @inheritDoc
944
     */
945
    protected function getDropColumnInstructions($tableName, $columnName)
946 48
    {
947
        $instructions = $this->beginAlterByCopyTable($tableName);
948 48
949 48
        $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
950
            $newState = $this->calculateNewTableColumns($tableName, $columnName, false);
951
952
            return $newState + $state;
953
        });
954 2
955
        $instructions->addPostStep(function ($state) use ($columnName) {
956 2
            $sql = preg_replace(
957
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
958
                '',
959
                $state['createSQL']
960
            );
961
962 48
            if (substr($sql, -2) === ', ') {
963
                $sql = substr($sql, 0, -2) . ')';
964 48
            }
965 47
966 47
            $this->execute($sql);
967 48
968
            return $state;
969
        });
970
971
        return $this->copyAndDropTmpTable($instructions, $tableName);
972
    }
973
974
    /**
975 42
     * Get an array of indexes from a particular table.
976
     *
977 42
     * @param string $tableName Table name
978 8
     *
979 42
     * @return array
980 42
     */
981 42
    protected function getIndexes($tableName)
982 42
    {
983
        $indexes = [];
984
        $schema = $this->getSchemaName($tableName, true)['schema'];
985
        $indexList = $this->getTableInfo($tableName, 'index_list');
986
987
        foreach ($indexList as $index) {
988
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
989
            $cols = [];
990
            foreach ($indexData as $indexItem) {
991 42
                $cols[] = $indexItem['name'];
992
            }
993 42
            $indexes[$index['name']] = $cols;
994 42
        }
995 42
996 42
        return $indexes;
997
    }
998
999 42
    /**
1000 42
     * Finds the names of a table's indexes matching the supplied columns
1001 42
     *
1002 42
     * @param string $tableName The table to which the index belongs
1003 42
     * @param string|string[] $columns The columns of the index
1004 4
     *
1005 4
     * @return array
1006
     */
1007 42
    protected function resolveIndex($tableName, $columns)
1008
    {
1009 42
        $columns = array_map('strtolower', (array)$columns);
1010 42
        $indexes = $this->getIndexes($tableName);
1011 42
        $matches = [];
1012
1013 42
        foreach ($indexes as $name => $index) {
1014
            $indexCols = array_map('strtolower', $index);
1015
            if ($columns == $indexCols) {
1016
                $matches[] = $name;
1017 42
            }
1018
        }
1019 42
1020
        return $matches;
1021
    }
1022
1023
    /**
1024
     * @inheritDoc
1025
     */
1026
    public function hasIndex($tableName, $columns)
1027
    {
1028 42
        return (bool)$this->resolveIndex($tableName, $columns);
1029
    }
1030 42
1031 2
    /**
1032
     * @inheritDoc
1033 42
     */
1034
    public function hasIndexByName($tableName, $indexName)
1035
    {
1036
        $indexName = strtolower($indexName);
1037
        $indexes = $this->getIndexes($tableName);
1038
1039
        foreach (array_keys($indexes) as $index) {
1040
            if ($indexName === strtolower($index)) {
1041
                return true;
1042 8
            }
1043
        }
1044 8
1045 2
        return false;
1046 2
    }
1047 6
1048
    /**
1049 8
     * @inheritDoc
1050 3
     */
1051 3
    protected function getAddIndexInstructions(Table $table, Index $index)
1052 6
    {
1053 6
        $indexColumnArray = [];
1054 6
        foreach ($index->getColumns() as $column) {
1055 6
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
1056 6
        }
1057
        $indexColumns = implode(',', $indexColumnArray);
1058 8
        $sql = sprintf(
1059 8
            'CREATE %s ON %s (%s)',
1060
            $this->getIndexSqlDefinition($table, $index),
1061
            $this->quoteTableName($table->getName()),
1062
            $indexColumns
1063
        );
1064
1065 47
        return new AlterInstructions([], [$sql]);
1066
    }
1067 47
1068
    /**
1069
     * @inheritDoc
1070
     */
1071
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1072
    {
1073
        $instructions = new AlterInstructions();
1074
        $indexNames = $this->resolveIndex($tableName, $columns);
1075
        $schema = $this->getSchemaName($tableName, true)['schema'];
1076 5
        foreach ($indexNames as $indexName) {
1077
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1078 5
                $instructions->addPostStep(sprintf(
1079 5
                    'DROP INDEX %s%s',
1080
                    $schema,
1081
                    $this->quoteColumnName($indexName)
1082 5
                ));
1083 5
            }
1084 5
        }
1085 5
1086 5
        return $instructions;
1087 5
    }
1088 5
1089 5
    /**
1090 5
     * @inheritDoc
1091 5
     */
1092 5
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1093 1
    {
1094 1
        $instructions = new AlterInstructions();
1095 5
        $indexName = strtolower($indexName);
1096 1
        $indexes = $this->getIndexes($tableName);
1097 1
1098
        $found = false;
1099 5
        foreach (array_keys($indexes) as $index) {
1100
            if ($indexName === strtolower($index)) {
1101
                $found = true;
1102
                break;
1103
            }
1104
        }
1105
1106
        if ($found) {
1107
            $schema = $this->getSchemaName($tableName, true)['schema'];
1108
                $instructions->addPostStep(sprintf(
1109
                    'DROP INDEX %s%s',
1110
                    $schema,
1111
                    $this->quoteColumnName($indexName)
1112
                ));
1113
        }
1114
1115
        return $instructions;
1116
    }
1117
1118
    /**
1119
     * {@inheritDoc}
1120
     *
1121
     * @throws \InvalidArgumentException
1122
     */
1123
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1124
    {
1125
        if ($constraint !== null) {
1126
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1127
        }
1128
1129
        $columns = array_map('strtolower', (array)$columns);
1130
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1131
1132
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1133
            return false;
1134
        }
1135
1136
        return true;
1137
    }
1138
1139
    /**
1140
     * Get the primary key from a particular table.
1141
     *
1142
     * @param string $tableName Table name
1143
     *
1144
     * @return string[]
1145
     */
1146
    protected function getPrimaryKey($tableName)
1147
    {
1148
        $primaryKey = [];
1149
1150
        $rows = $this->getTableInfo($tableName);
1151
1152
        foreach ($rows as $row) {
1153
            if ($row['pk'] > 0) {
1154
                $primaryKey[$row['pk'] - 1] = $row['name'];
1155
            }
1156
        }
1157
1158
        return $primaryKey;
1159
    }
1160
1161
    /**
1162
     * {@inheritDoc}
1163
     *
1164
     * @throws \InvalidArgumentException
1165
     */
1166
    public function hasForeignKey($tableName, $columns, $constraint = null)
1167
    {
1168
        if ($constraint !== null) {
1169
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1170
        }
1171
1172
        $columns = array_map('strtolower', (array)$columns);
1173
        $foreignKeys = $this->getForeignKeys($tableName);
1174
1175
        foreach ($foreignKeys as $key) {
1176
            $key = array_map('strtolower', $key);
1177
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1178
                continue;
1179
            }
1180
1181
            return true;
1182
        }
1183
1184
        return false;
1185
    }
1186
1187
    /**
1188
     * Get an array of foreign keys from a particular table.
1189
     *
1190
     * @param string $tableName Table name
1191
     *
1192
     * @return array
1193
     */
1194
    protected function getForeignKeys($tableName)
1195
    {
1196
        $foreignKeys = [];
1197
1198
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1199
1200
        foreach ($rows as $row) {
1201
            if (!isset($foreignKeys[$row['id']])) {
1202
                $foreignKeys[$row['id']] = [];
1203
            }
1204
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1205
        }
1206
1207
        return $foreignKeys;
1208
    }
1209
1210
    /**
1211
     * @param \Phinx\Db\Table\Table $table The Table
1212
     * @param string $column Column Name
1213
     *
1214
     * @return \Phinx\Db\Util\AlterInstructions
1215
     */
1216
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1217
    {
1218
        $instructions = $this->beginAlterByCopyTable($table->getName());
1219
1220
        $tableName = $table->getName();
1221
        $instructions->addPostStep(function ($state) use ($column) {
1222
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1223
1224
            $sql = $state['createSQL'];
1225
1226
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1227
                if (isset($matches[2])) {
1228
                    if ($matches[2] === 'INTEGER') {
1229
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1230
                    } else {
1231
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1232
                    }
1233
1234
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1235
                }
1236
            }
1237
1238
            $this->execute($sql);
1239
1240
            return $state;
1241
        });
1242
1243
        $instructions->addPostStep(function ($state) {
1244
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1245
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1246
            $selectColumns = $writeColumns = $names;
1247
1248
            return compact('selectColumns', 'writeColumns') + $state;
1249
        });
1250
1251
        return $this->copyAndDropTmpTable($instructions, $tableName);
1252
    }
1253
1254
    /**
1255
     * @param \Phinx\Db\Table\Table $table Table
1256
     * @param string $column Column Name
1257
     *
1258
     * @return \Phinx\Db\Util\AlterInstructions
1259
     */
1260
    protected function getDropPrimaryKeyInstructions($table, $column)
1261
    {
1262
        $instructions = $this->beginAlterByCopyTable($table->getName());
1263
1264
        $instructions->addPostStep(function ($state) {
1265
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1266
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1267
1268
            if ($sql) {
1269
                $this->execute($sql);
1270
            }
1271
1272
            return $state;
1273
        });
1274
1275
        $instructions->addPostStep(function ($state) use ($column) {
1276
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1277
1278
            return $newState + $state;
1279
        });
1280
1281
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1282
    }
1283
1284
    /**
1285
     * @inheritDoc
1286
     */
1287
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1288
    {
1289
        $instructions = $this->beginAlterByCopyTable($table->getName());
1290
1291
        $tableName = $table->getName();
1292
        $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
1293
            $this->execute('pragma foreign_keys = ON');
1294
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
1295
1296
            //Delete indexes from original table and recreate them in temporary table
1297
            $schema = $this->getSchemaName($tableName, true)['schema'];
1298
            $tmpTableName = $state['tmpTableName'];
1299
            $indexes = $this->getIndexes($tableName);
1300
            foreach (array_keys($indexes) as $indexName) {
1301
                $sql .= sprintf(
1302
                    'DROP INDEX %s%s; ',
1303
                    $schema,
1304
                    $this->quoteColumnName($indexName)
1305
                );
1306
                $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $indexName);
1307
                $sql .= preg_replace(
1308
                    "/\b${tableName}\b/",
1309
                    $tmpTableName,
1310
                    $createIndexSQL
1311
                );
1312
            }
1313
1314
            $this->execute($sql);
1315
1316
            return $state;
1317
        });
1318
1319
        $instructions->addPostStep(function ($state) {
1320
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1321
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1322
            $selectColumns = $writeColumns = $names;
1323
1324
            return compact('selectColumns', 'writeColumns') + $state;
1325
        });
1326
1327
        return $this->copyAndDropTmpTable($instructions, $tableName);
1328
    }
1329
1330
    /**
1331
     * @inheritDoc
1332
     *
1333
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
1334
     *
1335
     * @throws \BadMethodCallException
1336
     */
1337
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1338
    {
1339
        throw new BadMethodCallException('SQLite does not have named foreign keys');
1340
    }
1341
1342
    /**
1343
     * {@inheritDoc}
1344
     *
1345
     * @throws \InvalidArgumentException
1346
     */
1347
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1348
    {
1349
        $instructions = $this->beginAlterByCopyTable($tableName);
1350
1351
        $instructions->addPostStep(function ($state) use ($columns) {
1352
            $sql = '';
1353
1354
            foreach ($columns as $columnName) {
1355
                $search = sprintf(
1356
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1357
                    $this->quoteColumnName($columnName)
1358
                );
1359
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1360
            }
1361
1362
            if ($sql) {
1363
                $this->execute($sql);
1364
            }
1365
1366
            return $state;
1367
        });
1368
1369
        $instructions->addPostStep(function ($state) use ($columns) {
1370
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1371
1372
            $selectColumns = $newState['selectColumns'];
1373
            $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...
1374
            $diff = array_diff($columns, $selectColumns);
1375
1376
            if (!empty($diff)) {
1377
                throw new InvalidArgumentException(sprintf(
1378
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1379
                ));
1380
            }
1381
1382
            return $newState + $state;
1383
        });
1384
1385
        return $this->copyAndDropTmpTable($instructions, $tableName);
1386
    }
1387
1388
    /**
1389
     * {@inheritDoc}
1390
     *
1391
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1392
     */
1393
    public function getSqlType($type, $limit = null)
1394
    {
1395
        $typeLC = strtolower($type);
1396
        if ($type instanceof Literal) {
1397
            $name = $type;
1398
        } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1399
            $name = self::$supportedColumnTypes[$typeLC];
1400
        } elseif (in_array($typeLC, self::$unsupportedColumnTypes, true)) {
1401
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1402
        } else {
1403
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1404
        }
1405
1406
        return ['name' => $name, 'limit' => $limit];
1407
    }
1408
1409
    /**
1410
     * Returns Phinx type by SQL type
1411
     *
1412
     * @param string|null $sqlTypeDef SQL Type definition
1413
     *
1414
     * @return array
1415
     */
1416
    public function getPhinxType($sqlTypeDef)
1417
    {
1418
        $limit = null;
1419
        $scale = null;
1420
        if ($sqlTypeDef === null) {
1421
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1422
            $name = null;
1423
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1424
            // doesn't match the pattern of a type we'd know about
1425
            $name = Literal::from($sqlTypeDef);
1426
        } else {
1427
            // possibly a known type
1428
            $type = $match[1];
1429
            $typeLC = strtolower($type);
1430
            $affinity = $match[2] ?? '';
1431
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1432
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1433
            if (in_array($typeLC, ['tinyint', 'tinyinteger'], true) && $limit === 1) {
1434
                // the type is a MySQL-style boolean
1435
                $name = static::PHINX_TYPE_BOOLEAN;
1436
                $limit = null;
1437
            } elseif (isset(self::$supportedColumnTypes[$typeLC])) {
1438
                // the type is an explicitly supported type
1439
                $name = $typeLC;
1440
            } elseif (isset(self::$supportedColumnTypeAliases[$typeLC])) {
1441
                // the type is an alias for a supported type
1442
                $name = self::$supportedColumnTypeAliases[$typeLC];
1443
            } elseif (in_array($typeLC, self::$unsupportedColumnTypes, true)) {
1444
                // unsupported but known types are passed through lowercased, and without appended affinity
1445
                $name = Literal::from($typeLC);
1446
            } else {
1447
                // unknown types are passed through as-is
1448
                $name = Literal::from($type . $affinity);
1449
            }
1450
        }
1451
1452
        return [
1453
            'name' => $name,
1454
            'limit' => $limit,
1455
            'scale' => $scale,
1456
        ];
1457
    }
1458
1459
    /**
1460
     * @inheritDoc
1461
     */
1462
    public function createDatabase($name, $options = [])
1463
    {
1464
        touch($name . $this->suffix);
1465
    }
1466
1467
    /**
1468
     * @inheritDoc
1469
     */
1470
    public function hasDatabase($name)
1471
    {
1472
        return is_file($name . $this->suffix);
1473
    }
1474
1475
    /**
1476
     * @inheritDoc
1477
     */
1478
    public function dropDatabase($name)
1479
    {
1480
        if ($this->getOption('memory')) {
1481
            $this->disconnect();
1482
            $this->connect();
1483
        }
1484
        if (file_exists($name . $this->suffix)) {
1485
            unlink($name . $this->suffix);
1486
        }
1487
    }
1488
1489
    /**
1490
     * Gets the SQLite Column Definition for a Column object.
1491
     *
1492
     * @param \Phinx\Db\Table\Column $column Column
1493
     *
1494
     * @return string
1495
     */
1496
    protected function getColumnSqlDefinition(Column $column)
1497
    {
1498
        $isLiteralType = $column->getType() instanceof Literal;
1499
        if ($isLiteralType) {
1500
            $def = (string)$column->getType();
1501
        } else {
1502
            $sqlType = $this->getSqlType($column->getType());
1503
            $def = strtoupper($sqlType['name']);
1504
1505
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits, true);
1506
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1507
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1508
            }
1509
        }
1510
        if ($column->getPrecision() && $column->getScale()) {
1511
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1512
        }
1513
1514
        $default = $column->getDefault();
1515
1516
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1517
        $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...
1518
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1519
1520
        $def .= $this->getCommentDefinition($column);
1521
1522
        return $def;
1523
    }
1524
1525
    /**
1526
     * Gets the comment Definition for a Column object.
1527
     *
1528
     * @param \Phinx\Db\Table\Column $column Column
1529
     *
1530
     * @return string
1531
     */
1532
    protected function getCommentDefinition(Column $column)
1533
    {
1534
        if ($column->getComment()) {
1535
            return ' /* ' . $column->getComment() . ' */ ';
1536
        }
1537
1538
        return '';
1539
    }
1540
1541
    /**
1542
     * Gets the SQLite Index Definition for an Index object.
1543
     *
1544
     * @param \Phinx\Db\Table\Table $table Table
1545
     * @param \Phinx\Db\Table\Index $index Index
1546
     *
1547
     * @return string
1548
     */
1549
    protected function getIndexSqlDefinition(Table $table, Index $index)
1550
    {
1551
        if ($index->getType() === Index::UNIQUE) {
1552
            $def = 'UNIQUE INDEX';
1553
        } else {
1554
            $def = 'INDEX';
1555
        }
1556
        if (is_string($index->getName())) {
1557
            $indexName = $index->getName();
1558
        } else {
1559
            $indexName = $table->getName() . '_';
1560
            foreach ($index->getColumns() as $column) {
1561
                $indexName .= $column . '_';
1562
            }
1563
            $indexName .= 'index';
1564
        }
1565
        $def .= ' `' . $indexName . '`';
1566
1567
        return $def;
1568
    }
1569
1570
    /**
1571
     * @inheritDoc
1572
     */
1573
    public function getColumnTypes()
1574
    {
1575
        return array_keys(static::$supportedColumnTypes);
1576
    }
1577
1578
    /**
1579
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1580
     *
1581
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1582
     *
1583
     * @return string
1584
     */
1585
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1586
    {
1587
        $def = '';
1588
        if ($foreignKey->getConstraint()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $foreignKey->getConstraint() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1589
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1590
        }
1591
        $columnNames = [];
1592
        foreach ($foreignKey->getColumns() as $column) {
1593
            $columnNames[] = $this->quoteColumnName($column);
1594
        }
1595
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1596
        $refColumnNames = [];
1597
        foreach ($foreignKey->getReferencedColumns() as $column) {
1598
            $refColumnNames[] = $this->quoteColumnName($column);
1599
        }
1600
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1601
        if ($foreignKey->getOnDelete()) {
1602
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1603
        }
1604
        if ($foreignKey->getOnUpdate()) {
1605
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1606
        }
1607
1608
        return $def;
1609
    }
1610
1611
    /**
1612
     * @inheritDoc
1613
     */
1614
    public function getDecoratedConnection()
1615
    {
1616
        $options = $this->getOptions();
1617
        $options['quoteIdentifiers'] = true;
1618
1619
        if (!empty($options['name'])) {
1620
            $options['database'] = $options['name'];
1621
1622
            if (file_exists($options['name'] . $this->suffix)) {
1623
                $options['database'] = $options['name'] . $this->suffix;
1624
            }
1625
        }
1626
1627
        if ($this->connection === null) {
1628
            throw new RuntimeException('You need to connect first.');
1629
        }
1630
1631
        $driver = new SqliteDriver($options);
1632
        $driver->setConnection($this->connection);
1633
1634
        return new Connection(['driver' => $driver] + $options);
1635
    }
1636
}
1637