Passed
Pull Request — master (#2053)
by
unknown
02:58
created

SQLiteAdapter::connect()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 28
ccs 15
cts 15
cp 1
rs 8.8333
cc 7
nc 6
nop 0
crap 7
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_DECIMAL => 'decimal',
51
        self::PHINX_TYPE_DOUBLE => 'double',
52
        self::PHINX_TYPE_FLOAT => 'float',
53
        self::PHINX_TYPE_INTEGER => 'integer',
54
        self::PHINX_TYPE_JSON => 'json_text',
55
        self::PHINX_TYPE_JSONB => 'jsonb_text',
56 42
        self::PHINX_TYPE_SMALL_INTEGER => 'smallinteger',
57
        self::PHINX_TYPE_STRING => 'varchar',
58 42
        self::PHINX_TYPE_TEXT => 'text',
59 42
        self::PHINX_TYPE_TIME => 'time_text',
60
        self::PHINX_TYPE_TIMESTAMP => 'timestamp_text',
61
        self::PHINX_TYPE_TINY_INTEGER => 'tinyinteger',
62
        self::PHINX_TYPE_UUID => 'uuid_text',
63
        self::PHINX_TYPE_VARBINARY => 'varbinary_blob',
64
    ];
65 42
66 42
    /**
67
     * List of aliases of supported column types
68
     *
69 42
     * @var string[]
70
     */
71
    protected static $supportedColumnTypeAliases = [
72 42
        'varchar' => self::PHINX_TYPE_STRING,
73 42
        'tinyint' => self::PHINX_TYPE_TINY_INTEGER,
74 42
        'tinyinteger' => self::PHINX_TYPE_TINY_INTEGER,
75 42
        'smallint' => self::PHINX_TYPE_SMALL_INTEGER,
76
        'int' => self::PHINX_TYPE_INTEGER,
77
        'mediumint' => self::PHINX_TYPE_INTEGER,
78
        'mediuminteger' => self::PHINX_TYPE_INTEGER,
79 42
        'bigint' => self::PHINX_TYPE_BIG_INTEGER,
80 42
        'tinytext' => self::PHINX_TYPE_TEXT,
81
        'mediumtext' => self::PHINX_TYPE_TEXT,
82
        'longtext' => self::PHINX_TYPE_TEXT,
83
        'tinyblob' => self::PHINX_TYPE_BLOB,
84
        'mediumblob' => self::PHINX_TYPE_BLOB,
85
        'longblob' => self::PHINX_TYPE_BLOB,
86
        'real' => self::PHINX_TYPE_FLOAT,
87 42
    ];
88 42
89 42
    /**
90
     * List of known but unsupported Phinx column types
91
     *
92
     * @var string[]
93
     */
94 48
    protected static $unsupportedColumnTypes = [
95
        self::PHINX_TYPE_BIT,
96 48
        self::PHINX_TYPE_CIDR,
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
    /**
123
     * @var string
124
     */
125
    protected $suffix = '.sqlite3';
126
127
    /**
128
     * Indicates whether the database library version is at least the specified version
129
     *
130
     * @param string $ver The version to check against e.g. '3.28.0'
131
     * @return bool
132
     */
133
    public function databaseVersionAtLeast($ver)
134 43
    {
135
        $actual = $this->query('SELECT sqlite_version()')->fetchColumn();
136 43
137
        return version_compare($actual, $ver, '>=');
138
    }
139
140
    /**
141
     * {@inheritDoc}
142 44
     *
143
     * @throws \RuntimeException
144 44
     * @throws \InvalidArgumentException
145
     * @return void
146
     */
147
    public function connect()
148
    {
149
        if ($this->connection === null) {
150 42
            if (!class_exists('PDO') || !in_array('sqlite', PDO::getAvailableDrivers(), true)) {
151
                // @codeCoverageIgnoreStart
152 42
                throw new RuntimeException('You need to enable the PDO_SQLITE extension for Phinx to run properly.');
153 42
                // @codeCoverageIgnoreEnd
154 42
            }
155 12
156 42
            $options = $this->getOptions();
157
158 42
            // use a memory database if the option was specified
159
            if (!empty($options['memory']) || $options['name'] === static::MEMORY) {
160
                $params = [];
161
                if (!empty($options['cache'])) {
162
                    $params[] = 'cache=' . $options['cache'];
163
                }
164 42
                if ($params) {
165
                    if (PHP_VERSION_ID < 80100) {
166
                        throw new RuntimeException('SQLite URI support requires PHP 8.1.');
167 42
                    }
168 42
                    $dsn = 'sqlite:file:' . static::MEMORY . '?' . implode('&', $params);
169 42
                } else {
170 35
                }
171 35
                    $dsn = 'sqlite:' . static::MEMORY;
172 35
                }
173 35
            } else {
174
                $dsn = 'sqlite:' . $options['name'] . $this->suffix;
175 35
            }
176 42
177
            $driverOptions = [];
178 1
179 1
            // use custom data fetch mode
180 1
            if (!empty($options['fetch_mode'])) {
181 1
                $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode']));
182
            }
183 1
184 1
            $db = $this->createPdoConnection($dsn, null, null, $driverOptions);
185
186
            $this->setConnection($db);
187 42
        }
188 42
    }
189 42
190 42
    /**
191 42
     * @inheritDoc
192
     */
193
    public function setOptions(array $options)
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_PUBLIC on line 193 at column 4
Loading history...
194 42
    {
195 42
        parent::setOptions($options);
196 42
197 42
        if (isset($options['suffix'])) {
198 42
            $this->suffix = $options['suffix'];
199 42
        }
200
        //don't "fix" the file extension if it is blank, some people
201
        //might want a SQLITE db file with absolutely no extension.
202 1
        if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) {
203 1
            $this->suffix = '.' . $this->suffix;
204 1
        }
205
206 1
        return $this;
207 1
    }
208 1
209 1
    /**
210 1
     * @inheritDoc
211 1
     */
212 42
    public function disconnect()
213 42
    {
214 37
        $this->connection = null;
215
    }
216
217
    /**
218 42
     * @inheritDoc
219 42
     */
220 1
    public function hasTransactions()
221 1
    {
222 1
        return true;
223 1
    }
224
225 42
    /**
226
     * @inheritDoc
227 42
     */
228
    public function beginTransaction()
229 42
    {
230 6
        $this->getConnection()->beginTransaction();
231 42
    }
232 42
233
    /**
234
     * @inheritDoc
235
     */
236
    public function commitTransaction()
237 1
    {
238
        $this->getConnection()->commit();
239 1
    }
240 1
241
    /**
242
     * @inheritDoc
243
     */
244
    public function rollbackTransaction()
245 1
    {
246
        $this->getConnection()->rollBack();
247 1
    }
248 1
249
    /**
250
     * @inheritDoc
251
     */
252
    public function quoteTableName($tableName)
253 1
    {
254
        return str_replace('.', '`.`', $this->quoteColumnName($tableName));
255 1
    }
256 1
257 1
    /**
258 1
     * @inheritDoc
259
     */
260 1
    public function quoteColumnName($columnName)
261 1
    {
262
        return '`' . str_replace('`', '``', $columnName) . '`';
263
    }
264
265
    /**
266 1
     * @param string $tableName Table name
267
     * @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
268 1
     * @return array
269 1
     */
270
    protected function getSchemaName($tableName, $quoted = false)
271 1
    {
272 1
        if (preg_match("/.\.([^\.]+)$/", $tableName, $match)) {
273 1
            $table = $match[1];
274 1
            $schema = substr($tableName, 0, strlen($tableName) - strlen($match[0]) + 1);
275 1
            $result = ['schema' => $schema, 'table' => $table];
276 1
        } else {
277
            $result = ['schema' => '', 'table' => $tableName];
278 1
        }
279 1
280 1
        if ($quoted) {
281
            $result['schema'] = $result['schema'] !== '' ? $this->quoteColumnName($result['schema']) . '.' : '';
282 1
            $result['table'] = $this->quoteColumnName($result['table']);
283 1
        }
284 1
285
        return $result;
286 1
    }
287 1
288
    /**
289 1
     * Retrieves information about a given table from one of the SQLite pragmas
290
     *
291
     * @param string $tableName The table to query
292
     * @param string $pragma The pragma to query
293
     * @return array
294
     */
295 8
    protected function getTableInfo($tableName, $pragma = 'table_info')
296
    {
297 8
        $info = $this->getSchemaName($tableName, true);
298 8
299 8
        return $this->fetchAll(sprintf('PRAGMA %s%s(%s)', $info['schema'], $pragma, $info['table']));
300 7
    }
301
302 8
    /**
303
     * Searches through all available schemata to find a table and returns an array
304 8
     * containing the bare schema name and whether the table exists at all.
305
     * If no schema was specified and the table does not exist the "main" schema is returned
306
     *
307
     * @param string $tableName The name of the table to find
308
     * @return array
309
     */
310 4
    protected function resolveTable($tableName)
311
    {
312 4
        $info = $this->getSchemaName($tableName);
313 4
        if ($info['schema'] === '') {
314 4
            // if no schema is specified we search all schemata
315 4
            $rows = $this->fetchAll('PRAGMA database_list;');
316 4
            // the temp schema is always first to be searched
317 4
            $schemata = ['temp'];
318
            foreach ($rows as $row) {
319 4
                if (strtolower($row['name']) !== 'temp') {
320 4
                    $schemata[] = $row['name'];
321
                }
322
            }
323
            $defaultSchema = 'main';
324
        } else {
325 2
            // otherwise we search just the specified schema
326
            $schemata = (array)$info['schema'];
327 2
            $defaultSchema = $info['schema'];
328
        }
329 2
330
        $table = strtolower($info['table']);
331 2
        foreach ($schemata as $schema) {
332 2
            if (strtolower($schema) === 'temp') {
333 2
                $master = 'sqlite_temp_master';
334 2
            } else {
335 2
                $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master');
336 2
            }
337
            try {
338 2
                $rows = $this->fetchAll(sprintf("SELECT name FROM %s WHERE type='table' AND lower(name) = %s", $master, $this->quoteString($table)));
339 2
            } catch (PDOException $e) {
340 2
                // an exception can occur if the schema part of the table refers to a database which is not attached
341 2
                break;
342 2
            }
343 2
344 2
            // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined,
345 2
            // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII
346 2
            foreach ($rows as $row) {
347
                if (strtolower($row['name']) === $table) {
348 2
                    return ['schema' => $schema, 'table' => $row['name'], 'exists' => true];
349 1
                }
350
            }
351 1
        }
352
353
        return ['schema' => $defaultSchema, 'table' => $info['table'], 'exists' => false];
354 1
    }
355
356 1
    /**
357 1
     * @inheritDoc
358 1
     */
359
    public function hasTable($tableName)
360 1
    {
361 1
        return $this->hasCreatedTable($tableName) || $this->resolveTable($tableName)['exists'];
362
    }
363
364 1
    /**
365 1
     * @inheritDoc
366 1
     */
367 1
    public function createTable(Table $table, array $columns = [], array $indexes = [])
368 1
    {
369
        // Add the default primary key
370 1
        $options = $table->getOptions();
371
        if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) {
372 1
            $options['id'] = 'id';
373
        }
374 1
375 1
        if (isset($options['id']) && is_string($options['id'])) {
376
            // Handle id => "field_name" to support AUTO_INCREMENT
377
            $column = new Column();
378
            $column->setName($options['id'])
379
                   ->setType('integer')
380 6
                   ->setIdentity(true);
381
382
            array_unshift($columns, $column);
383
        }
384 6
385
        $sql = 'CREATE TABLE ';
386 6
        $sql .= $this->quoteTableName($table->getName()) . ' (';
387
        foreach ($columns as $column) {
388 6
            $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', ';
389 6
390 6
            if (isset($options['primary_key']) && $column->getIdentity()) {
391 6
                //remove column from the primary key array as it is already defined as an autoincrement
392 6
                //primary id
393 6
                $identityColumnIndex = array_search($column->getName(), $options['primary_key'], true);
394
                if ($identityColumnIndex !== false) {
395 6
                    unset($options['primary_key'][$identityColumnIndex]);
396 6
397 6
                    if (empty($options['primary_key'])) {
398 6
                        //The last primary key has been removed
399 6
                        unset($options['primary_key']);
400 6
                    }
401 6
                }
402 6
            }
403 6
        }
404
405 6
        // set the primary key(s)
406
        if (isset($options['primary_key'])) {
407
            $sql = rtrim($sql);
408
            $sql .= ' PRIMARY KEY (';
409
            if (is_string($options['primary_key'])) { // handle primary_key => 'id'
410
                $sql .= $this->quoteColumnName($options['primary_key']);
411 6
            } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id')
412
                $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key']));
413 6
            }
414 6
            $sql .= ')';
415 6
        } else {
416 6
            $sql = substr(rtrim($sql), 0, -1); // no primary keys
417
        }
418 6
419
        $sql = rtrim($sql) . ');';
420 6
        // execute the sql
421
        $this->execute($sql);
422 6
423 6
        foreach ($indexes as $index) {
424 6
            $this->addIndex($table, $index);
425 6
        }
426 6
427
        $this->addCreatedTable($table->getName());
428 6
    }
429
430 6
    /**
431 6
     * {@inheritDoc}
432 6
     *
433
     * @throws \InvalidArgumentException
434
     */
435
    protected function getChangePrimaryKeyInstructions(Table $table, $newColumns)
436
    {
437 2
        $instructions = new AlterInstructions();
438
439
        // Drop the existing primary key
440 2
        $primaryKey = $this->getPrimaryKey($table->getName());
441
        if (!empty($primaryKey)) {
442 2
            $instructions->merge(
443
                // FIXME: array access is a hack to make this incomplete implementation work with a correct getPrimaryKey implementation
444 2
                $this->getDropPrimaryKeyInstructions($table, $primaryKey[0])
445 2
            );
446 2
        }
447 2
448 2
        // Add the primary key(s)
449 2
        if (!empty($newColumns)) {
450
            if (!is_string($newColumns)) {
451 2
                throw new InvalidArgumentException(sprintf(
452 2
                    'Invalid value for primary key: %s',
453 2
                    json_encode($newColumns)
454 2
                ));
455 2
            }
456 2
457 2
            $instructions->merge(
458 2
                $this->getAddPrimaryKeyInstructions($table, $newColumns)
459 2
            );
460
        }
461 2
462
        return $instructions;
463 2
    }
464
465
    /**
466
     * {@inheritDoc}
467
     *
468
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
469 2
     *
470
     * @throws \BadMethodCallException
471 2
     */
472 2
    protected function getChangeCommentInstructions(Table $table, $newComment)
473 2
    {
474
        throw new BadMethodCallException('SQLite does not have table comments');
475 2
    }
476
477 2
    /**
478 2
     * @inheritDoc
479 2
     */
480
    protected function getRenameTableInstructions($tableName, $newTableName)
481 2
    {
482
        $this->updateCreatedTableName($tableName, $newTableName);
483 2
        $sql = sprintf(
484 2
            'ALTER TABLE %s RENAME TO %s',
485 2
            $this->quoteTableName($tableName),
486 2
            $this->quoteTableName($newTableName)
487 2
        );
488
489 2
        return new AlterInstructions([], [$sql]);
490
    }
491 2
492 2
    /**
493 2
     * @inheritDoc
494
     */
495
    protected function getDropTableInstructions($tableName)
496
    {
497
        $this->removeCreatedTable($tableName);
498
        $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
499
500
        return new AlterInstructions([], [$sql]);
501 9
    }
502
503 9
    /**
504 9
     * @inheritDoc
505
     */
506 9
    public function truncateTable($tableName)
507 9
    {
508 9
        $info = $this->resolveTable($tableName);
509 9
        // first try deleting the rows
510 9
        $this->execute(sprintf(
511 9
            'DELETE FROM %s.%s',
512 9
            $this->quoteColumnName($info['schema']),
513 9
            $this->quoteColumnName($info['table'])
514 9
        ));
515 9
516
        // assuming no error occurred, reset the autoincrement (if any)
517
        if ($this->hasTable($info['schema'] . '.sqlite_sequence')) {
518
            $this->execute(sprintf(
519
                'DELETE FROM %s.%s where name  = %s',
520
                $this->quoteColumnName($info['schema']),
521 9
                'sqlite_sequence',
522
                $this->quoteString($info['table'])
523 9
            ));
524 4
        }
525 4
    }
526
527 9
    /**
528 9
     * Parses a default-value expression to yield either a Literal representing
529
     * a string value, a string representing an expression, or some other scalar
530 9
     *
531 9
     * @param mixed $v The default-value expression to interpret
532 9
     * @param string $t The Phinx type of the column
533 9
     * @return mixed
534
     */
535 8
    protected function parseDefaultValue($v, $t)
536
    {
537 8
        if ($v === null) {
538
            return null;
539
        }
540
541
        // split the input into tokens
542
        $trimChars = " \t\n\r\0\x0B";
543 1
        $pattern = <<<PCRE_PATTERN
544
            /
545 1
                '(?:[^']|'')*'|                 # String literal
546
                "(?:[^"]|"")*"|                 # Standard identifier
547 1
                `(?:[^`]|``)*`|                 # MySQL identifier
548 1
                \[[^\]]*\]|                     # SQL Server identifier
549 1
                --[^\r\n]*|                     # Single-line comment
550
                \/\*(?:\*(?!\/)|[^\*])*\*\/|    # Multi-line comment
551
                [^\/\-]+|                       # Non-special characters
552
                .                               # Any other single character
553
            /sx
554
PCRE_PATTERN;
555
        preg_match_all($pattern, $v, $matches);
556
        // strip out any comment tokens
557
        $matches = array_map(function ($v) {
558
            return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v;
559 8
        }, $matches[0]);
560
        // reconstitute the string, trimming whitespace as well as parentheses
561 8
        $vClean = trim(implode('', $matches));
562 8
        $vBare = rtrim(ltrim($vClean, $trimChars . '('), $trimChars . ')');
563 8
564 8
        // match the string against one of several patterns
565 8
        if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $vBare)) {
566 8
            // magic date or time
567 8
            return strtoupper($vBare);
568 8
        } elseif (preg_match('/^\'(?:[^\']|\'\')*\'$/i', $vBare)) {
569 8
            // string literal
570 8
            $str = str_replace("''", "'", substr($vBare, 1, strlen($vBare) - 2));
571
572 8
            return Literal::from($str);
573 8
        } elseif (preg_match('/^[+-]?\d+$/i', $vBare)) {
574 8
            $int = (int)$vBare;
575
            // integer literal
576
            if ($t === self::PHINX_TYPE_BOOLEAN && ($int === 0 || $int === 1)) {
577
                return (bool)$int;
578
            } else {
579 1
                return $int;
580
            }
581 1
        } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $vBare)) {
582 1
            // float literal
583 1
            return (float)$vBare;
584
        } elseif (preg_match('/^0x[0-9a-f]+$/i', $vBare)) {
585 1
            // hexadecimal literal
586 1
            return hexdec(substr($vBare, 2));
587
        } elseif (preg_match('/^null$/i', $vBare)) {
588 1
            // null literal
589 1
            return null;
590 1
        } elseif (preg_match('/^true|false$/i', $vBare)) {
591 1
            // boolean literal
592 1
            return filter_var($vClean, \FILTER_VALIDATE_BOOLEAN);
593 1
        } else {
594 1
            // any other expression: return the expression with parentheses, but without comments
595 1
            return Expression::from($vClean);
596 1
        }
597 1
    }
598
599
    /**
600
     * Returns the name of the specified table's identity column, or null if the table has no identity
601
     *
602
     * 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
603
     *
604
     * @param string $tableName The name of the table
605 1
     * @return string|null
606
     */
607 1
    protected function resolveIdentity($tableName)
608
    {
609 1
        $result = null;
610 1
        // make sure the table has only one primary key column which is of type integer
611 1
        foreach ($this->getTableInfo($tableName) as $col) {
612 1
            $type = strtolower($col['type']);
613 1
            if ($col['pk'] > 1) {
614 1
                // the table has a composite primary key
615 1
                return null;
616 1
            } elseif ($col['pk'] == 0) {
617 1
                // the column is not a primary key column and is thus not relevant
618
                continue;
619
            } elseif ($type !== 'integer') {
620
                // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias
621
                return null;
622
            } else {
623
                // the column is a candidate for a row ID alias
624
                $result = $col['name'];
625 5
            }
626
        }
627 5
        // if there is no suitable PK column, stop now
628
        if ($result === null) {
629
            return null;
630 5
        }
631
        // make sure the table does not have a PK-origin autoindex
632 5
        // such an autoindex would indicate either that the primary key was specified as descending, or that this is a WITHOUT ROWID table
633 5
        foreach ($this->getTableInfo($tableName, 'index_list') as $idx) {
634 5
            if ($idx['origin'] === 'pk') {
635
                return null;
636 1
            }
637
        }
638
639
        return $result;
640
    }
641
642
    /**
643
     * @inheritDoc
644
     */
645 5
    public function getColumns($tableName)
646
    {
647 5
        $columns = [];
648 5
649
        $rows = $this->getTableInfo($tableName);
650
        $identity = $this->resolveIdentity($tableName);
651
652
        foreach ($rows as $columnInfo) {
653
            $column = new Column();
654
            $type = $this->getPhinxType($columnInfo['type']);
655
            $default = $this->parseDefaultValue($columnInfo['dflt_value'], $type['name']);
656
657
            $column->setName($columnInfo['name'])
658
                // SQLite on PHP 8.1 returns int for notnull, older versions return a string
659
                   ->setNull((int)$columnInfo['notnull'] !== 1)
660
                   ->setDefault($default)
661 5
                   ->setType($type['name'])
662
                   ->setLimit($type['limit'])
663 5
                   ->setScale($type['scale'])
664 5
                   ->setIdentity($columnInfo['name'] === $identity);
665 5
666 5
            $columns[] = $column;
667 5
        }
668 5
669 5
        return $columns;
670 5
    }
671 5
672 5
    /**
673 5
     * @inheritDoc
674
     */
675
    public function hasColumn($tableName, $columnName)
676
    {
677
        $rows = $this->getTableInfo($tableName);
678
        foreach ($rows as $column) {
679 4
            if (strcasecmp($column['name'], $columnName) === 0) {
680
                return true;
681
            }
682 4
        }
683
684 4
        return false;
685 4
    }
686
687 4
    /**
688 4
     * @inheritDoc
689 4
     */
690 4
    protected function getAddColumnInstructions(Table $table, Column $column)
691 4
    {
692 4
        $tableName = $table->getName();
693
694 4
        $instructions = $this->beginAlterByCopyTable($tableName);
695 4
696 4
        $instructions->addPostStep(function ($state) use ($tableName, $column) {
697 4
            // we use the final column to anchor our regex to insert the new column,
698 4
            // as the alternative is unwinding all possible table constraints which
699
            // gets messy quickly with CHECK constraints.
700 4
            $columns = $this->getColumns($tableName);
701
            if (!$columns) {
702 4
                return $state;
703 4
            }
704
            $finalColumnName = end($columns)->getName();
705 4
            $sql = preg_replace(
706 4
                sprintf(
707 4
                    "/(%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+)([,)])/",
708 4
                    $this->quoteColumnName($finalColumnName)
709 4
                ),
710 4
                sprintf(
711 4
                    '$1, %s %s$2',
712
                    $this->quoteColumnName($column->getName()),
713 4
                    $this->getColumnSqlDefinition($column)
714 4
                ),
715 4
                $state['createSQL'],
716
                1
717
            );
718
            $this->execute($sql);
719
720 1
            return $state;
721
        });
722
723 1
        $instructions->addPostStep(function ($state) use ($tableName) {
724
            $newState = $this->calculateNewTableColumns($tableName, false, false);
725
726
            return $newState + $state;
727 1
        });
728
729 1
        return $this->copyAndDropTmpTable($instructions, $tableName);
730
    }
731 1
732 1
    /**
733 1
     * Returns the original CREATE statement for the give table
734 1
     *
735 1
     * @param string $tableName The table name to get the create statement for
736 1
     * @return string
737
     */
738 1
    protected function getDeclaringSql($tableName)
739 1
    {
740 1
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'table'");
741 1
742 1
        $sql = '';
743 1
        foreach ($rows as $table) {
744 1
            if ($table['tbl_name'] === $tableName) {
745
                $sql = $table['sql'];
746 1
            }
747
        }
748 1
749
        $columnsInfo = $this->getTableInfo($tableName);
750
751
        foreach ($columnsInfo as $column) {
752
            $columnName = $column['name'];
753
            $columnNamePattern = "\"$columnName\"|`$columnName`|\\[$columnName\\]|$columnName";
754 1
            $columnNamePattern = "#([\(,]+\\s*)($columnNamePattern)(\\s)#iU";
755
756 1
            $sql = preg_replace($columnNamePattern, "$1`$columnName`$3", $sql);
757 1
        }
758 1
759 1
        $tableNamePattern = "\"$tableName\"|`$tableName`|\\[$tableName\\]|$tableName";
760 1
        $tableNamePattern = "#^(CREATE TABLE)\s*($tableNamePattern)\s*(\()#Ui";
761 1
762 1
        $sql = preg_replace($tableNamePattern, "$1 `$tableName` $3", $sql, 1);
763
764 1
        return $sql;
765
    }
766 1
767 1
    /**
768 1
     * Returns the original CREATE statement for the give index
769 1
     *
770 1
     * @param string $tableName The table name to get the create statement for
771
     * @param string $indexName The table index
772 1
     * @return string
773
     */
774 1
    protected function getDeclaringIndexSql($tableName, $indexName)
775 1
    {
776 1
        $rows = $this->fetchAll("SELECT * FROM sqlite_master WHERE `type` = 'index'");
777
778
        $sql = '';
779
        foreach ($rows as $table) {
780
            if ($table['tbl_name'] === $tableName && $table['name'] === $indexName) {
781
                $sql = $table['sql'] . '; ';
782
            }
783
        }
784
785
        return $sql;
786
    }
787
788
    /**
789
     * Copies all the data from a tmp table to another table
790
     *
791
     * @param string $tableName The table name to copy the data to
792
     * @param string $tmpTableName The tmp table name where the data is stored
793
     * @param string[] $writeColumns The list of columns in the target table
794
     * @param string[] $selectColumns The list of columns in the tmp table
795
     * @return void
796
     */
797
    protected function copyDataToNewTable($tableName, $tmpTableName, $writeColumns, $selectColumns)
798
    {
799
        $sql = sprintf(
800
            'INSERT INTO %s(%s) SELECT %s FROM %s',
801
            $this->quoteTableName($tableName),
802
            implode(', ', $writeColumns),
803
            implode(', ', $selectColumns),
804
            $this->quoteTableName($tmpTableName)
805
        );
806
        $this->execute($sql);
807
    }
808
809
    /**
810 43
     * Modifies the passed instructions to copy all data from the table into
811
     * the provided tmp table and then drops the table and rename tmp table.
812
     *
813 43
     * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
814 42
     * @param string $tableName The table name to copy the data to
815
     * @return \Phinx\Db\Util\AlterInstructions
816 43
     */
817
    protected function copyAndDropTmpTable($instructions, $tableName)
818
    {
819 43
        $instructions->addPostStep(function ($state) use ($tableName) {
820 1
            $this->copyDataToNewTable(
821
                $state['tmpTableName'],
822 43
                $tableName,
823 38
                $state['writeColumns'],
824
                $state['selectColumns']
825 43
            );
826 42
827
            $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
828 43
            $this->execute(sprintf(
829 2
                'ALTER TABLE %s RENAME TO %s',
830
                $this->quoteTableName($state['tmpTableName']),
831 43
                $this->quoteTableName($tableName)
832 1
            ));
833
834 43
            return $state;
835 1
        });
836
837 43
        return $instructions;
838 42
    }
839
840 43
    /**
841 1
     * Returns the columns and type to use when copying a table to another in the process
842
     * of altering a table
843 43
     *
844 1
     * @param string $tableName The table to modify
845
     * @param string|false $columnName The column name that is about to change
846 43
     * @param string|false $newColumnName Optionally the new name for the column
847 43
     * @throws \InvalidArgumentException
848 1
     * @return array
849
     */
850 43
    protected function calculateNewTableColumns($tableName, $columnName, $newColumnName)
851 42
    {
852
        $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($tableName)));
853 5
        $selectColumns = [];
854
        $writeColumns = [];
855 5
        $columnType = null;
856 4
        $found = false;
857
858
        foreach ($columns as $column) {
859
            $selectName = $column['name'];
860 1
            $writeName = $selectName;
861 1
862
            if ($selectName === $columnName) {
863
                $writeName = $newColumnName;
864 1
                $found = true;
865
                $columnType = $column['type'];
866
                $selectName = $newColumnName === false ? $newColumnName : $selectName;
867 1
            }
868
869 1
            $selectColumns[] = $selectName;
870 1
            $writeColumns[] = $writeName;
871 1
        }
872
873
        $selectColumns = array_filter($selectColumns, 'strlen');
874
        $writeColumns = array_filter($writeColumns, 'strlen');
875
        $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns);
876
        $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns);
877
878
        if ($columnName && !$found) {
879
            throw new InvalidArgumentException(sprintf(
880 3
                'The specified column doesn\'t exist: ' . $columnName
881
            ));
882 3
        }
883 1
884
        return compact('writeColumns', 'selectColumns', 'columnType');
885 2
    }
886 2
887 2
    /**
888 2
     * Returns the initial instructions to alter a table using the
889 1
     * create-copy-drop strategy
890 1
     *
891 2
     * @param string $tableName The table to modify
892
     * @return \Phinx\Db\Util\AlterInstructions
893
     */
894 2
    protected function beginAlterByCopyTable($tableName)
895 2
    {
896 1
        $instructions = new AlterInstructions();
897 1
        $instructions->addPostStep(function ($state) use ($tableName) {
898
            $tmpTableName = "tmp_{$tableName}";
899
            $createSQL = $this->getDeclaringSql($tableName);
900 1
901 2
            // Table name in SQLite can be hilarious inside declaring SQL:
902
            // - tableName
903
            // - `tableName`
904
            // - "tableName"
905
            // - [this is a valid table name too!]
906
            // - etc.
907
            // Just remove all characters before first "(" and build them again
908
            $createSQL = preg_replace(
909
                "/^CREATE TABLE .* \(/Ui",
910 2
                '',
911
                $createSQL
912
            );
913
914
            $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}";
915
916 2
            return compact('createSQL', 'tmpTableName') + $state;
917 1
        });
918
919
        return $instructions;
920 1
    }
921 1
922 2
    /**
923 1
     * @inheritDoc
924 1
     */
925 2
    protected function getRenameColumnInstructions($tableName, $columnName, $newColumnName)
926 2
    {
927
        $instructions = $this->beginAlterByCopyTable($tableName);
928
929
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) {
930
            $sql = str_replace(
931
                $this->quoteColumnName($columnName),
932
                $this->quoteColumnName($newColumnName),
933 2
                $state['createSQL']
934
            );
935
            $this->execute($sql);
936 1
937 1
            return $state;
938
        });
939 1
940
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
941
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
942
943
            return $newState + $state;
944
        });
945
946 48
        return $this->copyAndDropTmpTable($instructions, $tableName);
947
    }
948 48
949 48
    /**
950
     * @inheritDoc
951
     */
952
    protected function getChangeColumnInstructions($tableName, $columnName, Column $newColumn)
953
    {
954 2
        $instructions = $this->beginAlterByCopyTable($tableName);
955
956 2
        $newColumnName = $newColumn->getName();
957
        $instructions->addPostStep(function ($state) use ($columnName, $newColumn) {
958
            $sql = preg_replace(
959
                sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)),
960
                sprintf('%s %s$1', $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn)),
961
                $state['createSQL'],
962 48
                1
963
            );
964 48
            $this->execute($sql);
965 47
966 47
            return $state;
967 48
        });
968
969
        $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) {
970
            $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName);
971
972
            return $newState + $state;
973
        });
974
975 42
        return $this->copyAndDropTmpTable($instructions, $tableName);
976
    }
977 42
978 8
    /**
979 42
     * @inheritDoc
980 42
     */
981 42
    protected function getDropColumnInstructions($tableName, $columnName)
982 42
    {
983
        $instructions = $this->beginAlterByCopyTable($tableName);
984
985
        $instructions->addPostStep(function ($state) use ($tableName, $columnName) {
986
            $newState = $this->calculateNewTableColumns($tableName, $columnName, false);
987
988
            return $newState + $state;
989
        });
990
991 42
        $instructions->addPostStep(function ($state) use ($columnName) {
992
            $sql = preg_replace(
993 42
                sprintf("/%s\s%s.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName)), preg_quote($state['columnType'])),
994 42
                '',
995 42
                $state['createSQL']
996 42
            );
997
998
            if (substr($sql, -2) === ', ') {
999 42
                $sql = substr($sql, 0, -2) . ')';
1000 42
            }
1001 42
1002 42
            $this->execute($sql);
1003 42
1004 4
            return $state;
1005 4
        });
1006
1007 42
        return $this->copyAndDropTmpTable($instructions, $tableName);
1008
    }
1009 42
1010 42
    /**
1011 42
     * Get an array of indexes from a particular table.
1012
     *
1013 42
     * @param string $tableName Table name
1014
     * @return array
1015
     */
1016
    protected function getIndexes($tableName)
1017 42
    {
1018
        $indexes = [];
1019 42
        $schema = $this->getSchemaName($tableName, true)['schema'];
1020
        $indexList = $this->getTableInfo($tableName, 'index_list');
1021
1022
        foreach ($indexList as $index) {
1023
            $indexData = $this->fetchAll(sprintf('pragma %sindex_info(%s)', $schema, $this->quoteColumnName($index['name'])));
1024
            $cols = [];
1025
            foreach ($indexData as $indexItem) {
1026
                $cols[] = $indexItem['name'];
1027
            }
1028 42
            $indexes[$index['name']] = $cols;
1029
        }
1030 42
1031 2
        return $indexes;
1032
    }
1033 42
1034
    /**
1035
     * Finds the names of a table's indexes matching the supplied columns
1036
     *
1037
     * @param string $tableName The table to which the index belongs
1038
     * @param string|string[] $columns The columns of the index
1039
     * @return array
1040
     */
1041
    protected function resolveIndex($tableName, $columns)
1042 8
    {
1043
        $columns = array_map('strtolower', (array)$columns);
1044 8
        $indexes = $this->getIndexes($tableName);
1045 2
        $matches = [];
1046 2
1047 6
        foreach ($indexes as $name => $index) {
1048
            $indexCols = array_map('strtolower', $index);
1049 8
            if ($columns == $indexCols) {
1050 3
                $matches[] = $name;
1051 3
            }
1052 6
        }
1053 6
1054 6
        return $matches;
1055 6
    }
1056 6
1057
    /**
1058 8
     * @inheritDoc
1059 8
     */
1060
    public function hasIndex($tableName, $columns)
1061
    {
1062
        return (bool)$this->resolveIndex($tableName, $columns);
1063
    }
1064
1065 47
    /**
1066
     * @inheritDoc
1067 47
     */
1068
    public function hasIndexByName($tableName, $indexName)
1069
    {
1070
        $indexName = strtolower($indexName);
1071
        $indexes = $this->getIndexes($tableName);
1072
1073
        foreach (array_keys($indexes) as $index) {
1074
            if ($indexName === strtolower($index)) {
1075
                return true;
1076 5
            }
1077
        }
1078 5
1079 5
        return false;
1080
    }
1081
1082 5
    /**
1083 5
     * @inheritDoc
1084 5
     */
1085 5
    protected function getAddIndexInstructions(Table $table, Index $index)
1086 5
    {
1087 5
        $indexColumnArray = [];
1088 5
        foreach ($index->getColumns() as $column) {
1089 5
            $indexColumnArray[] = sprintf('`%s` ASC', $column);
1090 5
        }
1091 5
        $indexColumns = implode(',', $indexColumnArray);
1092 5
        $sql = sprintf(
1093 1
            'CREATE %s ON %s (%s)',
1094 1
            $this->getIndexSqlDefinition($table, $index),
1095 5
            $this->quoteTableName($table->getName()),
1096 1
            $indexColumns
1097 1
        );
1098
1099 5
        return new AlterInstructions([], [$sql]);
1100
    }
1101
1102
    /**
1103
     * @inheritDoc
1104
     */
1105
    protected function getDropIndexByColumnsInstructions($tableName, $columns)
1106
    {
1107
        $instructions = new AlterInstructions();
1108
        $indexNames = $this->resolveIndex($tableName, $columns);
1109
        $schema = $this->getSchemaName($tableName, true)['schema'];
1110
        foreach ($indexNames as $indexName) {
1111
            if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1112
                $instructions->addPostStep(sprintf(
1113
                    'DROP INDEX %s%s',
1114
                    $schema,
1115
                    $this->quoteColumnName($indexName)
1116
                ));
1117
            }
1118
        }
1119
1120
        return $instructions;
1121
    }
1122
1123
    /**
1124
     * @inheritDoc
1125
     */
1126
    protected function getDropIndexByNameInstructions($tableName, $indexName)
1127
    {
1128
        $instructions = new AlterInstructions();
1129
        $indexName = strtolower($indexName);
1130
        $indexes = $this->getIndexes($tableName);
1131
1132
        $found = false;
1133
        foreach (array_keys($indexes) as $index) {
1134
            if ($indexName === strtolower($index)) {
1135
                $found = true;
1136
                break;
1137
            }
1138
        }
1139
1140
        if ($found) {
1141
            $schema = $this->getSchemaName($tableName, true)['schema'];
1142
                $instructions->addPostStep(sprintf(
1143
                    'DROP INDEX %s%s',
1144
                    $schema,
1145
                    $this->quoteColumnName($indexName)
1146
                ));
1147
        }
1148
1149
        return $instructions;
1150
    }
1151
1152
    /**
1153
     * {@inheritDoc}
1154
     *
1155
     * @throws \InvalidArgumentException
1156
     */
1157
    public function hasPrimaryKey($tableName, $columns, $constraint = null)
1158
    {
1159
        if ($constraint !== null) {
1160
            throw new InvalidArgumentException('SQLite does not support named constraints.');
1161
        }
1162
1163
        $columns = array_map('strtolower', (array)$columns);
1164
        $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName));
1165
1166
        if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) {
1167
            return false;
1168
        }
1169
1170
        return true;
1171
    }
1172
1173
    /**
1174
     * Get the primary key from a particular table.
1175
     *
1176
     * @param string $tableName Table name
1177
     * @return string[]
1178
     */
1179
    protected function getPrimaryKey($tableName)
1180
    {
1181
        $primaryKey = [];
1182
1183
        $rows = $this->getTableInfo($tableName);
1184
1185
        foreach ($rows as $row) {
1186
            if ($row['pk'] > 0) {
1187
                $primaryKey[$row['pk'] - 1] = $row['name'];
1188
            }
1189
        }
1190
1191
        return $primaryKey;
1192
    }
1193
1194
    /**
1195
     * @inheritDoc
1196
     */
1197
    public function hasForeignKey($tableName, $columns, $constraint = null)
1198
    {
1199
        if ($constraint !== null) {
1200
            return preg_match(
1201
                "/,?\sCONSTRAINT\s" . preg_quote($this->quoteColumnName($constraint)) . ' FOREIGN KEY/',
1202
                $this->getDeclaringSql($tableName)
1203
            ) === 1;
1204
        }
1205
1206
        $columns = array_map('strtolower', (array)$columns);
1207
        $foreignKeys = $this->getForeignKeys($tableName);
1208
1209
        foreach ($foreignKeys as $key) {
1210
            $key = array_map('strtolower', $key);
1211
            if (array_diff($key, $columns) || array_diff($columns, $key)) {
1212
                continue;
1213
            }
1214
1215
            return true;
1216
        }
1217
1218
        return false;
1219
    }
1220
1221
    /**
1222
     * Get an array of foreign keys from a particular table.
1223
     *
1224
     * @param string $tableName Table name
1225
     * @return array
1226
     */
1227
    protected function getForeignKeys($tableName)
1228
    {
1229
        $foreignKeys = [];
1230
1231
        $rows = $this->getTableInfo($tableName, 'foreign_key_list');
1232
1233
        foreach ($rows as $row) {
1234
            if (!isset($foreignKeys[$row['id']])) {
1235
                $foreignKeys[$row['id']] = [];
1236
            }
1237
            $foreignKeys[$row['id']][$row['seq']] = $row['from'];
1238
        }
1239
1240
        return $foreignKeys;
1241
    }
1242
1243
    /**
1244
     * @param \Phinx\Db\Table\Table $table The Table
1245
     * @param string $column Column Name
1246
     * @return \Phinx\Db\Util\AlterInstructions
1247
     */
1248
    protected function getAddPrimaryKeyInstructions(Table $table, $column)
1249
    {
1250
        $instructions = $this->beginAlterByCopyTable($table->getName());
1251
1252
        $tableName = $table->getName();
1253
        $instructions->addPostStep(function ($state) use ($column) {
1254
            $matchPattern = "/(`$column`)\s+(\w+(\(\d+\))?)\s+((NOT )?NULL)/";
1255
1256
            $sql = $state['createSQL'];
1257
1258
            if (preg_match($matchPattern, $state['createSQL'], $matches)) {
1259
                if (isset($matches[2])) {
1260
                    if ($matches[2] === 'INTEGER') {
1261
                        $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT';
1262
                    } else {
1263
                        $replace = '$1 $2 NOT NULL PRIMARY KEY';
1264
                    }
1265
1266
                    $sql = preg_replace($matchPattern, $replace, $state['createSQL'], 1);
1267
                }
1268
            }
1269
1270
            $this->execute($sql);
1271
1272
            return $state;
1273
        });
1274
1275
        $instructions->addPostStep(function ($state) {
1276
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1277
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1278
            $selectColumns = $writeColumns = $names;
1279
1280
            return compact('selectColumns', 'writeColumns') + $state;
1281
        });
1282
1283
        return $this->copyAndDropTmpTable($instructions, $tableName);
1284
    }
1285
1286
    /**
1287
     * @param \Phinx\Db\Table\Table $table Table
1288
     * @param string $column Column Name
1289
     * @return \Phinx\Db\Util\AlterInstructions
1290
     */
1291
    protected function getDropPrimaryKeyInstructions($table, $column)
1292
    {
1293
        $instructions = $this->beginAlterByCopyTable($table->getName());
1294
1295
        $instructions->addPostStep(function ($state) {
1296
            $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
1297
            $sql = preg_replace($search, '', $state['createSQL'], 1);
1298
1299
            if ($sql) {
1300
                $this->execute($sql);
1301
            }
1302
1303
            return $state;
1304
        });
1305
1306
        $instructions->addPostStep(function ($state) use ($column) {
1307
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column);
1308
1309
            return $newState + $state;
1310
        });
1311
1312
        return $this->copyAndDropTmpTable($instructions, $table->getName());
1313
    }
1314
1315
    /**
1316
     * @inheritDoc
1317
     */
1318
    protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey)
1319
    {
1320
        $instructions = $this->beginAlterByCopyTable($table->getName());
1321
1322
        $tableName = $table->getName();
1323
        $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
1324
            $this->execute('pragma foreign_keys = ON');
1325
            $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
1326
1327
            //Delete indexes from original table and recreate them in temporary table
1328
            $schema = $this->getSchemaName($tableName, true)['schema'];
1329
            $tmpTableName = $state['tmpTableName'];
1330
            $indexes = $this->getIndexes($tableName);
1331
            foreach (array_keys($indexes) as $indexName) {
1332
                if (strpos($indexName, 'sqlite_autoindex_') !== 0) {
1333
                    $sql .= sprintf(
1334
                        'DROP INDEX %s%s; ',
1335
                        $schema,
1336
                        $this->quoteColumnName($indexName)
1337
                    );
1338
                    $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $indexName);
1339
                    $sql .= preg_replace(
1340
                        "/\b${tableName}\b/",
1341
                        $tmpTableName,
1342
                        $createIndexSQL
1343
                    );
1344
                }
1345
            }
1346
1347
            $this->execute($sql);
1348
1349
            return $state;
1350
        });
1351
1352
        $instructions->addPostStep(function ($state) {
1353
            $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName'])));
1354
            $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name'));
1355
            $selectColumns = $writeColumns = $names;
1356
1357
            return compact('selectColumns', 'writeColumns') + $state;
1358
        });
1359
1360
        return $this->copyAndDropTmpTable($instructions, $tableName);
1361
    }
1362
1363
    /**
1364
     * {@inheritDoc}
1365
     *
1366
     * SQLiteAdapter does not implement this functionality, and so will always throw an exception if used.
1367
     *
1368
     * @throws \BadMethodCallException
1369
     */
1370
    protected function getDropForeignKeyInstructions($tableName, $constraint)
1371
    {
1372
        throw new BadMethodCallException('SQLite does not have named foreign keys');
1373
    }
1374
1375
    /**
1376
     * {@inheritDoc}
1377
     *
1378
     * @throws \InvalidArgumentException
1379
     */
1380
    protected function getDropForeignKeyByColumnsInstructions($tableName, $columns)
1381
    {
1382
        $instructions = $this->beginAlterByCopyTable($tableName);
1383
1384
        $instructions->addPostStep(function ($state) use ($columns) {
1385
            $sql = '';
1386
1387
            foreach ($columns as $columnName) {
1388
                $search = sprintf(
1389
                    "/,[^,]*\(%s(?:,`?(.*)`?)?\) REFERENCES[^,]*\([^\)]*\)[^,)]*/",
1390
                    $this->quoteColumnName($columnName)
1391
                );
1392
                $sql = preg_replace($search, '', $state['createSQL'], 1);
1393
            }
1394
1395
            if ($sql) {
1396
                $this->execute($sql);
1397
            }
1398
1399
            return $state;
1400
        });
1401
1402
        $instructions->addPostStep(function ($state) use ($columns) {
1403
            $newState = $this->calculateNewTableColumns($state['tmpTableName'], $columns[0], $columns[0]);
1404
1405
            $selectColumns = $newState['selectColumns'];
1406
            $columns = array_map([$this, 'quoteColumnName'], $columns);
1407
            $diff = array_diff($columns, $selectColumns);
1408
1409
            if (!empty($diff)) {
1410
                throw new InvalidArgumentException(sprintf(
1411
                    'The specified columns don\'t exist: ' . implode(', ', $diff)
1412
                ));
1413
            }
1414
1415
            return $newState + $state;
1416
        });
1417
1418
        return $this->copyAndDropTmpTable($instructions, $tableName);
1419
    }
1420
1421
    /**
1422
     * {@inheritDoc}
1423
     *
1424
     * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException
1425
     */
1426
    public function getSqlType($type, $limit = null)
1427
    {
1428
        $typeLC = strtolower($type);
1429
        if ($type instanceof Literal) {
1430
            $name = $type;
1431
        } elseif (isset(static::$supportedColumnTypes[$typeLC])) {
1432
            $name = static::$supportedColumnTypes[$typeLC];
1433
        } elseif (in_array($typeLC, static::$unsupportedColumnTypes, true)) {
1434
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by SQLite.');
1435
        } else {
1436
            throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not known by SQLite.');
1437
        }
1438
1439
        return ['name' => $name, 'limit' => $limit];
1440
    }
1441
1442
    /**
1443
     * Returns Phinx type by SQL type
1444
     *
1445
     * @param string|null $sqlTypeDef SQL Type definition
1446
     * @return array
1447
     */
1448
    public function getPhinxType($sqlTypeDef)
1449
    {
1450
        $limit = null;
1451
        $scale = null;
1452
        if ($sqlTypeDef === null) {
1453
            // in SQLite columns can legitimately have null as a type, which is distinct from the empty string
1454
            $name = null;
1455
        } elseif (!preg_match('/^([a-z]+)(_(?:integer|float|text|blob))?(?:\((\d+)(?:,(\d+))?\))?$/i', $sqlTypeDef, $match)) {
1456
            // doesn't match the pattern of a type we'd know about
1457
            $name = Literal::from($sqlTypeDef);
1458
        } else {
1459
            // possibly a known type
1460
            $type = $match[1];
1461
            $typeLC = strtolower($type);
1462
            $affinity = $match[2] ?? '';
1463
            $limit = isset($match[3]) && strlen($match[3]) ? (int)$match[3] : null;
1464
            $scale = isset($match[4]) && strlen($match[4]) ? (int)$match[4] : null;
1465
            if (in_array($typeLC, ['tinyint', 'tinyinteger'], true) && $limit === 1) {
1466
                // the type is a MySQL-style boolean
1467
                $name = static::PHINX_TYPE_BOOLEAN;
1468
                $limit = null;
1469
            } elseif (isset(static::$supportedColumnTypes[$typeLC])) {
1470
                // the type is an explicitly supported type
1471
                $name = $typeLC;
1472
            } elseif (isset(static::$supportedColumnTypeAliases[$typeLC])) {
1473
                // the type is an alias for a supported type
1474
                $name = static::$supportedColumnTypeAliases[$typeLC];
1475
            } elseif (in_array($typeLC, static::$unsupportedColumnTypes, true)) {
1476
                // unsupported but known types are passed through lowercased, and without appended affinity
1477
                $name = Literal::from($typeLC);
1478
            } else {
1479
                // unknown types are passed through as-is
1480
                $name = Literal::from($type . $affinity);
1481
            }
1482
        }
1483
1484
        return [
1485
            'name' => $name,
1486
            'limit' => $limit,
1487
            'scale' => $scale,
1488
        ];
1489
    }
1490
1491
    /**
1492
     * @inheritDoc
1493
     */
1494
    public function createDatabase($name, $options = [])
1495
    {
1496
        touch($name . $this->suffix);
1497
    }
1498
1499
    /**
1500
     * @inheritDoc
1501
     */
1502
    public function hasDatabase($name)
1503
    {
1504
        return is_file($name . $this->suffix);
1505
    }
1506
1507
    /**
1508
     * @inheritDoc
1509
     */
1510
    public function dropDatabase($name)
1511
    {
1512
        $this->createdTables = [];
1513
        if ($this->getOption('memory')) {
1514
            $this->disconnect();
1515
            $this->connect();
1516
        }
1517
        if (file_exists($name . $this->suffix)) {
1518
            unlink($name . $this->suffix);
1519
        }
1520
    }
1521
1522
    /**
1523
     * Gets the SQLite Column Definition for a Column object.
1524
     *
1525
     * @param \Phinx\Db\Table\Column $column Column
1526
     * @return string
1527
     */
1528
    protected function getColumnSqlDefinition(Column $column)
1529
    {
1530
        $isLiteralType = $column->getType() instanceof Literal;
1531
        if ($isLiteralType) {
1532
            $def = (string)$column->getType();
1533
        } else {
1534
            $sqlType = $this->getSqlType($column->getType());
1535
            $def = strtoupper($sqlType['name']);
1536
1537
            $limitable = in_array(strtoupper($sqlType['name']), $this->definitionsWithLimits, true);
1538
            if (($column->getLimit() || isset($sqlType['limit'])) && $limitable) {
1539
                $def .= '(' . ($column->getLimit() ?: $sqlType['limit']) . ')';
1540
            }
1541
        }
1542
        if ($column->getPrecision() && $column->getScale()) {
1543
            $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
1544
        }
1545
1546
        $default = $column->getDefault();
1547
1548
        $def .= $column->isNull() ? ' NULL' : ' NOT NULL';
1549
        $def .= $this->getDefaultValueDefinition($default, $column->getType());
1550
        $def .= $column->isIdentity() ? ' PRIMARY KEY AUTOINCREMENT' : '';
1551
1552
        $def .= $this->getCommentDefinition($column);
1553
1554
        return $def;
1555
    }
1556
1557
    /**
1558
     * Gets the comment Definition for a Column object.
1559
     *
1560
     * @param \Phinx\Db\Table\Column $column Column
1561
     * @return string
1562
     */
1563
    protected function getCommentDefinition(Column $column)
1564
    {
1565
        if ($column->getComment()) {
1566
            return ' /* ' . $column->getComment() . ' */ ';
1567
        }
1568
1569
        return '';
1570
    }
1571
1572
    /**
1573
     * Gets the SQLite Index Definition for an Index object.
1574
     *
1575
     * @param \Phinx\Db\Table\Table $table Table
1576
     * @param \Phinx\Db\Table\Index $index Index
1577
     * @return string
1578
     */
1579
    protected function getIndexSqlDefinition(Table $table, Index $index)
1580
    {
1581
        if ($index->getType() === Index::UNIQUE) {
1582
            $def = 'UNIQUE INDEX';
1583
        } else {
1584
            $def = 'INDEX';
1585
        }
1586
        if (is_string($index->getName())) {
1587
            $indexName = $index->getName();
1588
        } else {
1589
            $indexName = $table->getName() . '_';
1590
            foreach ($index->getColumns() as $column) {
1591
                $indexName .= $column . '_';
1592
            }
1593
            $indexName .= 'index';
1594
        }
1595
        $def .= ' `' . $indexName . '`';
1596
1597
        return $def;
1598
    }
1599
1600
    /**
1601
     * @inheritDoc
1602
     */
1603
    public function getColumnTypes()
1604
    {
1605
        return array_keys(static::$supportedColumnTypes);
1606
    }
1607
1608
    /**
1609
     * Gets the SQLite Foreign Key Definition for an ForeignKey object.
1610
     *
1611
     * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key
1612
     * @return string
1613
     */
1614
    protected function getForeignKeySqlDefinition(ForeignKey $foreignKey)
1615
    {
1616
        $def = '';
1617
        if ($foreignKey->getConstraint()) {
1618
            $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint());
1619
        }
1620
        $columnNames = [];
1621
        foreach ($foreignKey->getColumns() as $column) {
1622
            $columnNames[] = $this->quoteColumnName($column);
1623
        }
1624
        $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')';
1625
        $refColumnNames = [];
1626
        foreach ($foreignKey->getReferencedColumns() as $column) {
1627
            $refColumnNames[] = $this->quoteColumnName($column);
1628
        }
1629
        $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')';
1630
        if ($foreignKey->getOnDelete()) {
1631
            $def .= ' ON DELETE ' . $foreignKey->getOnDelete();
1632
        }
1633
        if ($foreignKey->getOnUpdate()) {
1634
            $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate();
1635
        }
1636
1637
        return $def;
1638
    }
1639
1640
    /**
1641
     * @inheritDoc
1642
     */
1643
    public function getDecoratedConnection()
1644
    {
1645
        $options = $this->getOptions();
1646
        $options['quoteIdentifiers'] = true;
1647
1648
        if (!empty($options['name'])) {
1649
            $options['database'] = $options['name'];
1650
1651
            if (file_exists($options['name'] . $this->suffix)) {
1652
                $options['database'] = $options['name'] . $this->suffix;
1653
            }
1654
        }
1655
1656
        if ($this->connection === null) {
1657
            throw new RuntimeException('You need to connect first.');
1658
        }
1659
1660
        $driver = new SqliteDriver($options);
1661
        $driver->setConnection($this->connection);
1662
1663
        return new Connection(['driver' => $driver] + $options);
1664
    }
1665
}
1666