2025-02-12 14:03:39 +00:00
|
|
|
<?php
|
|
|
|
namespace DatabaseHelper;
|
|
|
|
|
|
|
|
use DatabaseHelper\enums\Propagation;
|
|
|
|
use DatabaseHelper\enums\Type;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
class Migration
|
2025-02-12 14:03:39 +00:00
|
|
|
{
|
2025-02-12 14:55:28 +00:00
|
|
|
protected Schema $schema;
|
|
|
|
protected array $columnsToAdd = [];
|
|
|
|
protected array $columnsToModify = [];
|
|
|
|
protected array $columnsToDrop = [];
|
|
|
|
protected ?array $primaryKey = null;
|
|
|
|
protected array $foreignKeysToAdd = [];
|
|
|
|
protected array $foreignKeysToDrop = [];
|
|
|
|
|
|
|
|
public function __construct(Schema $table) {
|
|
|
|
$this->schema = $table->copy();
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
public function column(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration {
|
|
|
|
if ($this->schema->existsColumn($name))
|
|
|
|
throw new InvalidArgumentException("Column '$name' already exists.");
|
|
|
|
$this->schema->columns[$name] = [
|
|
|
|
'name' => $name,
|
|
|
|
'type' => $type,
|
|
|
|
'default' => $default,
|
2025-02-12 14:03:39 +00:00
|
|
|
'isNullable' => $isNullable,
|
2025-02-12 14:55:28 +00:00
|
|
|
'isUnique' => $isUnique
|
2025-02-12 14:03:39 +00:00
|
|
|
];
|
2025-02-12 14:55:28 +00:00
|
|
|
$this->columnsToAdd[] = $name;
|
2025-02-12 14:03:39 +00:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
public function modify(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration {
|
|
|
|
$this->schema->requireColumn($name);
|
|
|
|
$this->schema->columns[$name] = [
|
|
|
|
'name' => $name,
|
|
|
|
'type' => $type,
|
|
|
|
'default' => $default,
|
|
|
|
'isNullable' => $isNullable,
|
|
|
|
'isUnique' => $isUnique
|
2025-02-12 14:03:39 +00:00
|
|
|
];
|
2025-02-12 14:55:28 +00:00
|
|
|
$this->columnsToModify[] = $name;
|
|
|
|
return $this;
|
|
|
|
}
|
2025-02-12 14:03:39 +00:00
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
public function delete(string $name): Migration {
|
|
|
|
$this->schema->requireColumn($name);
|
|
|
|
unset($this->schema->columns[$name]);
|
|
|
|
$this->columnsToDrop[] = $name;
|
|
|
|
return $this;
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Migration {
|
2025-02-12 14:03:39 +00:00
|
|
|
$name = $foreignTable->primaryKey();
|
2025-02-12 14:55:28 +00:00
|
|
|
if ($this->schema->existsColumn($name))
|
|
|
|
throw new InvalidArgumentException("Column '$name' already exists.");
|
2025-02-12 14:03:39 +00:00
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
$this->schema->foreignKeys[$name] = [
|
|
|
|
'name' => $name,
|
|
|
|
'table' => $foreignTable->name,
|
2025-02-12 14:03:39 +00:00
|
|
|
'onDelete' => $onDelete,
|
|
|
|
'onUpdate' => $onUpdate
|
|
|
|
];
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
$this->schema->columns[$name] = $foreignTable->columns[$name];
|
|
|
|
$this->foreignKeysToAdd[] = $name;
|
2025-02-12 14:03:39 +00:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
public function dereference(Schema $foreignTable): Migration {
|
|
|
|
$name = $foreignTable->primaryKey();
|
|
|
|
if ($this->schema->existsReference($foreignTable))
|
|
|
|
throw new InvalidArgumentException('Foreign table is not referenced.');
|
|
|
|
unset($this->schema->foreignKeys[$name]);
|
|
|
|
// Also remove the column and mark it for dropping.
|
|
|
|
if (isset($this->schema->columns[$name]))
|
|
|
|
$this->delete($name);
|
|
|
|
$this->foreignKeysToDrop[] = $name;
|
2025-02-12 14:03:39 +00:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function toSql(): string {
|
2025-02-12 14:55:28 +00:00
|
|
|
global $wpdb;
|
|
|
|
// We assume that the table name in the schema is not prefixed; add the prefix here.
|
|
|
|
$tableName = $wpdb->prefix . $this->schema->name;
|
|
|
|
$clauses = [];
|
|
|
|
|
|
|
|
// Process new columns.
|
|
|
|
foreach ($this->columnsToAdd as $columnName) {
|
|
|
|
$col = $this->schema->columns[$columnName];
|
|
|
|
$clause = "ADD COLUMN `{$col['name']}` " . $col['type']->toString();
|
|
|
|
if (!$col['isNullable']) {
|
|
|
|
$clause .= " NOT NULL";
|
|
|
|
}
|
|
|
|
if ($col['isUnique']) {
|
|
|
|
$clause .= " UNIQUE";
|
|
|
|
}
|
|
|
|
if ($col['default'] !== null) {
|
|
|
|
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
|
|
|
|
$clause .= " DEFAULT $default";
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
2025-02-12 14:55:28 +00:00
|
|
|
$clauses[] = $clause;
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
// Process modified columns.
|
|
|
|
foreach ($this->columnsToModify as $columnName) {
|
|
|
|
$col = $this->schema->columns[$columnName];
|
|
|
|
$clause = "MODIFY COLUMN `{$col['name']}` " . $col['type']->toString();
|
|
|
|
if (!$col['isNullable']) {
|
|
|
|
$clause .= " NOT NULL";
|
|
|
|
}
|
|
|
|
if ($col['isUnique']) {
|
|
|
|
$clause .= " UNIQUE";
|
|
|
|
}
|
|
|
|
if ($col['default'] !== null) {
|
|
|
|
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
|
|
|
|
$clause .= " DEFAULT $default";
|
|
|
|
}
|
|
|
|
$clauses[] = $clause;
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
// Process dropped columns.
|
|
|
|
foreach ($this->columnsToDrop as $columnName) {
|
|
|
|
$clauses[] = "DROP COLUMN `{$columnName}`";
|
|
|
|
}
|
2025-02-12 14:03:39 +00:00
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
// Process foreign keys to add.
|
|
|
|
foreach ($this->foreignKeysToAdd as $fkName) {
|
|
|
|
$fk = $this->schema->foreignKeys[$fkName];
|
|
|
|
// Here we name the constraint “fk_{$name}”. (There are other acceptable naming schemes.)
|
|
|
|
$clause = "ADD CONSTRAINT `fk_{$fk['name']}` FOREIGN KEY (`{$fk['name']}`) REFERENCES `{$fk['table']}` (`{$fk['name']}`) ON DELETE " . $fk['onDelete']->toString() . " ON UPDATE " . $fk['onUpdate']->toString();
|
|
|
|
$clauses[] = $clause;
|
|
|
|
}
|
2025-02-12 14:03:39 +00:00
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
// Process foreign keys to drop.
|
|
|
|
foreach ($this->foreignKeysToDrop as $fkName) {
|
|
|
|
// Again, we assume the constraint was named “fk_{$name}”
|
|
|
|
$clauses[] = "DROP FOREIGN KEY `fk_{$fkName}`";
|
|
|
|
}
|
2025-02-12 14:03:39 +00:00
|
|
|
|
2025-02-12 14:55:28 +00:00
|
|
|
if (empty($clauses)) {
|
|
|
|
throw new InvalidArgumentException("No migration operations to perform.");
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
2025-02-12 14:55:28 +00:00
|
|
|
|
|
|
|
$sql = "ALTER TABLE `{$tableName}`\n" . implode(",\n", $clauses) . ";";
|
|
|
|
return esc_sql($sql);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function drop(): null {
|
|
|
|
global $wpdb;
|
|
|
|
$tableName = $wpdb->prefix . $this->schema->name;
|
|
|
|
$sql = "DROP TABLE IF EXISTS `{$tableName}`;";
|
|
|
|
$wpdb->query(esc_sql($sql));
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function migrate(): Schema {
|
|
|
|
global $wpdb;
|
|
|
|
$sql = $this->toSql();
|
|
|
|
$wpdb->query($sql);
|
|
|
|
return $this->schema;
|
2025-02-12 14:03:39 +00:00
|
|
|
}
|
|
|
|
}
|