|
|
|
@@ -18,6 +18,7 @@ class SqliteMigration extends Migration |
|
|
|
protected SqliteConnection $connection; |
|
|
|
protected string $table; |
|
|
|
protected string $type; |
|
|
|
protected array $drops = []; |
|
|
|
|
|
|
|
public function __construct(SqliteConnection $connection, string $table, string $type) |
|
|
|
{ |
|
|
|
@@ -28,52 +29,135 @@ class SqliteMigration extends Migration |
|
|
|
|
|
|
|
public function execute() |
|
|
|
{ |
|
|
|
$command = $this->type === 'create' ? '' : 'ALTER TABLE'; |
|
|
|
if ($this->type === 'create') { |
|
|
|
$this->executeCreate(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
$fields = array_map(fn($field) => $this->stringForField($field), $this->fields); |
|
|
|
$hasAlteredField = array_reduce($this->fields, fn($carry, $field) => $carry || $field->alter, false); |
|
|
|
|
|
|
|
if ($this->type === 'create') { |
|
|
|
$fields = join(',' . PHP_EOL, $fields); |
|
|
|
if (count($this->drops) > 0 || $hasAlteredField) { |
|
|
|
$this->executeRebuild(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
$this->executeAdd(); |
|
|
|
} |
|
|
|
|
|
|
|
private function executeCreate() |
|
|
|
{ |
|
|
|
$fields = join(',' . PHP_EOL, array_map(fn($field) => $this->columnDefinition($field), $this->fields)); |
|
|
|
|
|
|
|
$query = " |
|
|
|
CREATE TABLE \"{$this->table}\" ( |
|
|
|
{$fields} |
|
|
|
); |
|
|
|
"; |
|
|
|
|
|
|
|
$this->connection->pdo()->prepare($query)->execute(); |
|
|
|
} |
|
|
|
|
|
|
|
$query = " |
|
|
|
CREATE TABLE \"{$this->table}\" ( |
|
|
|
{$fields} |
|
|
|
); |
|
|
|
"; |
|
|
|
private function executeAdd() |
|
|
|
{ |
|
|
|
$fields = join(';' . PHP_EOL, array_map( |
|
|
|
fn($field) => "ADD COLUMN {$this->columnDefinition($field)}", |
|
|
|
$this->fields |
|
|
|
)); |
|
|
|
|
|
|
|
$query = " |
|
|
|
ALTER TABLE \"{$this->table}\" |
|
|
|
{$fields}; |
|
|
|
"; |
|
|
|
|
|
|
|
$this->connection->pdo()->prepare($query)->execute(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* SQLite can't alter or drop columns in place, so the standard approach is to |
|
|
|
* rebuild the table: create a new table with the desired schema, copy the |
|
|
|
* surviving data across, then swap it in for the original. |
|
|
|
*/ |
|
|
|
private function executeRebuild() |
|
|
|
{ |
|
|
|
$pdo = $this->connection->pdo(); |
|
|
|
|
|
|
|
$existingColumns = $pdo->query("PRAGMA table_info(\"{$this->table}\")")->fetchAll(); |
|
|
|
|
|
|
|
$alteredFields = []; |
|
|
|
foreach ($this->fields as $field) { |
|
|
|
if ($field->alter) { |
|
|
|
$alteredFields[$field->name] = $field; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if ($this->type === 'alter') { |
|
|
|
$fields = join(';' . PHP_EOL, $fields); |
|
|
|
$newFields = array_filter($this->fields, fn($field) => !$field->alter); |
|
|
|
|
|
|
|
$keptColumnNames = []; |
|
|
|
$definitions = []; |
|
|
|
|
|
|
|
$query = " |
|
|
|
ALTER TABLE \"{$this->table}\" |
|
|
|
{$fields}; |
|
|
|
"; |
|
|
|
foreach ($existingColumns as $column) { |
|
|
|
$name = $column['name']; |
|
|
|
|
|
|
|
if (in_array($name, $this->drops)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
$keptColumnNames[] = $name; |
|
|
|
|
|
|
|
$definitions[] = isset($alteredFields[$name]) |
|
|
|
? $this->columnDefinition($alteredFields[$name]) |
|
|
|
: $this->existingColumnDefinition($column); |
|
|
|
} |
|
|
|
|
|
|
|
foreach ($newFields as $field) { |
|
|
|
$definitions[] = $this->columnDefinition($field); |
|
|
|
} |
|
|
|
|
|
|
|
$statement = $this->connection->pdo()->prepare($query); |
|
|
|
$statement->execute(); |
|
|
|
$tempTable = "{$this->table}_migration_tmp"; |
|
|
|
$definitionList = join(',' . PHP_EOL, $definitions); |
|
|
|
$columnList = join(', ', array_map(fn($name) => "\"{$name}\"", $keptColumnNames)); |
|
|
|
|
|
|
|
$pdo->beginTransaction(); |
|
|
|
|
|
|
|
$pdo->exec("CREATE TABLE \"{$tempTable}\" ({$definitionList})"); |
|
|
|
$pdo->exec("INSERT INTO \"{$tempTable}\" ({$columnList}) SELECT {$columnList} FROM \"{$this->table}\""); |
|
|
|
$pdo->exec("DROP TABLE \"{$this->table}\""); |
|
|
|
$pdo->exec("ALTER TABLE \"{$tempTable}\" RENAME TO \"{$this->table}\""); |
|
|
|
|
|
|
|
$pdo->commit(); |
|
|
|
} |
|
|
|
|
|
|
|
private function stringForField(Field $field): string |
|
|
|
private function existingColumnDefinition(array $column): string |
|
|
|
{ |
|
|
|
$prefix = ''; |
|
|
|
$name = $column['name']; |
|
|
|
$type = $column['type']; |
|
|
|
|
|
|
|
if ($this->type === 'alter') { |
|
|
|
$prefix = 'ADD COLUMN'; |
|
|
|
if ($column['pk'] == 1 && $type === 'INTEGER') { |
|
|
|
return "\"{$name}\" INTEGER PRIMARY KEY AUTOINCREMENT"; |
|
|
|
} |
|
|
|
|
|
|
|
if ($field->alter) { |
|
|
|
throw new MigrationException('SQLite doesn\'t support altering columns'); |
|
|
|
$definition = "\"{$name}\" {$type}"; |
|
|
|
|
|
|
|
if ($column['notnull'] == 1) { |
|
|
|
$definition .= " NOT NULL"; |
|
|
|
} |
|
|
|
|
|
|
|
if ($column['dflt_value'] !== null) { |
|
|
|
$definition .= " DEFAULT {$column['dflt_value']}"; |
|
|
|
} |
|
|
|
|
|
|
|
return $definition; |
|
|
|
} |
|
|
|
|
|
|
|
private function columnDefinition(Field $field): string |
|
|
|
{ |
|
|
|
if ($field instanceof BoolField) { |
|
|
|
$template = "{$prefix} \"{$field->name}\" INTEGER"; |
|
|
|
$template = "\"{$field->name}\" INTEGER"; |
|
|
|
|
|
|
|
if (!$field->nullable) { |
|
|
|
$template .= " NOT NULL"; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ($field->default !== null) { |
|
|
|
$default = (int) $field->default; |
|
|
|
$template .= " DEFAULT {$default}"; |
|
|
|
@@ -83,12 +167,12 @@ class SqliteMigration extends Migration |
|
|
|
} |
|
|
|
|
|
|
|
if ($field instanceof DateTimeField) { |
|
|
|
$template = "{$prefix} \"{$field->name}\" TEXT"; |
|
|
|
$template = "\"{$field->name}\" TEXT"; |
|
|
|
|
|
|
|
if (!$field->nullable) { |
|
|
|
$template .= " NOT NULL"; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ($field->default === 'CURRENT_TIMESTAMP') { |
|
|
|
$template .= " DEFAULT CURRENT_TIMESTAMP"; |
|
|
|
} else if ($field->default !== null) { |
|
|
|
@@ -99,12 +183,12 @@ class SqliteMigration extends Migration |
|
|
|
} |
|
|
|
|
|
|
|
if ($field instanceof FloatField) { |
|
|
|
$template = "{$prefix} \"{$field->name}\" REAL"; |
|
|
|
$template = "\"{$field->name}\" REAL"; |
|
|
|
|
|
|
|
if (!$field->nullable) { |
|
|
|
$template .= " NOT NULL"; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ($field->default !== null) { |
|
|
|
$template .= " DEFAULT {$field->default}"; |
|
|
|
} |
|
|
|
@@ -113,16 +197,16 @@ class SqliteMigration extends Migration |
|
|
|
} |
|
|
|
|
|
|
|
if ($field instanceof IdField) { |
|
|
|
return "{$prefix} \"{$field->name}\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE"; |
|
|
|
return "\"{$field->name}\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE"; |
|
|
|
} |
|
|
|
|
|
|
|
if ($field instanceof IntField) { |
|
|
|
$template = "{$prefix} \"{$field->name}\" INTEGER"; |
|
|
|
$template = "\"{$field->name}\" INTEGER"; |
|
|
|
|
|
|
|
if (!$field->nullable) { |
|
|
|
$template .= " NOT NULL"; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ($field->default !== null) { |
|
|
|
$template .= " DEFAULT {$field->default}"; |
|
|
|
} |
|
|
|
@@ -131,14 +215,14 @@ class SqliteMigration extends Migration |
|
|
|
} |
|
|
|
|
|
|
|
if ($field instanceof StringField || $field instanceof TextField) { |
|
|
|
$template = "{$prefix} \"{$field->name}\" TEXT"; |
|
|
|
$template = "\"{$field->name}\" TEXT"; |
|
|
|
|
|
|
|
if (!$field->nullable) { |
|
|
|
$template .= " NOT NULL"; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ($field->default !== null) { |
|
|
|
$template .= " DEFAULT '{$field->default}'"; |
|
|
|
$template .= " DEFAULT '{$field->default}'"; |
|
|
|
} |
|
|
|
|
|
|
|
return $template; |
|
|
|
@@ -149,6 +233,7 @@ class SqliteMigration extends Migration |
|
|
|
|
|
|
|
public function dropColumn(string $name): static |
|
|
|
{ |
|
|
|
throw new MigrationException('SQLite doesn\'t support dropping columns'); |
|
|
|
$this->drops[] = $name; |
|
|
|
return $this; |
|
|
|
} |
|
|
|
} |