finish migration class

This commit is contained in:
Jan-Niclas Loosen 2025-02-12 16:55:03 +01:00
parent c4a7025978
commit fe9661d611
5 changed files with 196 additions and 148 deletions

View File

@ -26,7 +26,7 @@ class Deletion
if (!$this->isConditioned()) if (!$this->isConditioned())
throw new InvalidArgumentException("Deletions need to be conditioned."); throw new InvalidArgumentException("Deletions need to be conditioned.");
return esc_sql("DELETE FROM $table WHERE $whereClause"); return "DELETE FROM $table WHERE $whereClause";
} }
/** /**

View File

@ -11,7 +11,6 @@ class Migration
protected array $columnsToAdd = []; protected array $columnsToAdd = [];
protected array $columnsToModify = []; protected array $columnsToModify = [];
protected array $columnsToDrop = []; protected array $columnsToDrop = [];
protected array $primaryKey = null;
protected array $foreignKeysToAdd = []; protected array $foreignKeysToAdd = [];
protected array $foreignKeysToDrop = []; protected array $foreignKeysToDrop = [];
@ -19,34 +18,40 @@ class Migration
$this->schema = $table->copy(); $this->schema = $table->copy();
} }
public function add(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration { public function column(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration {
if ($this->schema->existsColumn($name)) if ($this->schema->existsColumn($name))
throw new InvalidArgumentException("Column '$name' already exists."); throw new InvalidArgumentException("Column '$name' already exists.");
$this->schema->columns[$name] = [ $this->schema->columns[$name] = [
'name' => $name, 'name' => $name,
'type' => $type, 'type' => $type,
'default' => $default, 'default' => $default,
'isNullable' => $isNullable, 'isNullable' => $isNullable,
'isUnique' => $isUnique 'isUnique' => $isUnique
]; ];
$this->columnsToAdd[] = $name; $this->columnsToAdd[] = $name;
return $this; return $this;
} }
public function modify(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration { public function modify(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration {
$this->schema->requireColumn($name); $this->schema->requireColumn($name);
if (isset($this->schema->foreignKeys[$name]))
throw new InvalidArgumentException('Referencing columns cannot be modified.');
$this->schema->columns[$name] = [ $this->schema->columns[$name] = [
'name' => $name, 'name' => $name,
'type' => $type, 'type' => $type,
'default' => $default, 'default' => $default,
'isNullable' => $isNullable, 'isNullable' => $isNullable,
'isUnique' => $isUnique 'isUnique' => $isUnique
]; ];
$this->columnsToModify[] = $name; $this->columnsToModify[] = $name;
return $this; return $this;
} }
public function delete(string $name): Migration { public function drop(string $name): Migration {
$this->schema->requireColumn($name); $this->schema->requireColumn($name);
unset($this->schema->columns[$name]); unset($this->schema->columns[$name]);
$this->columnsToDrop[] = $name; $this->columnsToDrop[] = $name;
@ -54,20 +59,97 @@ class Migration
} }
public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Migration { public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Migration {
$name = $foreignTable->primaryKey();
$this->schema->requireColumn($name);
$this->schema->foreignKeys[$name] = [
'name' => $name,
'table' => $foreignTable->name,
'onDelete' => $onDelete,
'onUpdate' => $onUpdate
];
$this->schema->columns[$name] = $foreignTable->columns[$name];
$this->foreignKeysToAdd[] = $name;
return $this;
} }
public function dereference(Schema $foreignTable): Migration { 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]);
$this->drop($name);
$this->foreignKeysToDrop[] = $name;
return $this;
} }
public function toSql(): string { public function toSql(): string {
}
public function drop(): null {
global $wpdb; 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()}";
$clause .= !$col['isNullable'] ? " NOT NULL" : "";
$clause .= $col['isUnique'] ? " UNIQUE" : "";
if (!is_null($col['default'])) {
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
$clause .= " DEFAULT $default";
}
$clauses[] = $clause;
}
// Process modified columns.
foreach ($this->columnsToModify as $columnName) {
$col = $this->schema->columns[$columnName];
$clause = "MODIFY COLUMN `{$col['name']}` {$col['type']->toString()}";
$clause .= !$col['isNullable'] ? " NOT NULL" : "";
$clause .= $col['isUnique'] ? " UNIQUE" : "";
if (!is_null($col['default'])) {
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
$clause .= " DEFAULT $default";
}
$clauses[] = $clause;
}
// Process dropped columns.
foreach ($this->columnsToDrop as $columnName)
$clauses[] = "DROP COLUMN `$columnName`";
// Process foreign keys to add.
foreach ($this->foreignKeysToAdd as $key) {
$foreignKey = $this->schema->foreignKeys[$key];
$clause = " ADD CONSTRAINT `fk_{$foreignKey['name']}`";
$clause .= " FOREIGN KEY (`{$foreignKey['name']}`)";
$clause .= " REFERENCES `{$foreignKey['table']}` (`{$foreignKey['name']}`)";
$clause .= " ON DELETE {$foreignKey['onDelete']->toString()}";
$clause .= " ON UPDATE {$foreignKey['onUpdate']->toString()},\n";
$clauses[] = $clause;
}
// Process foreign keys to drop.
foreach ($this->foreignKeysToDrop as $fkName)
$clauses[] = "DROP FOREIGN KEY `fk_{$fkName}`";
if (empty($clauses))
throw new InvalidArgumentException("No migration operations to perform.");
return "ALTER TABLE `$tableName`\n" . implode(",\n", $clauses) . ";";
} }
public function migrate(): Schema { public function migrate(): Schema {

View File

@ -75,26 +75,26 @@ class Query
// Build the SELECT clause. // Build the SELECT clause.
$columns = implode(", ", $selectColumns); $columns = implode(", ", $selectColumns);
$primaryTable = $this->schema->name; $primaryTable = $this->schema->name;
$sqlStatement = "SELECT $columns FROM $primaryTable"; $query = "SELECT $columns FROM $primaryTable";
// Append join clauses, if any. // Append join clauses, if any.
if ($this->isJoined()) if ($this->isJoined())
foreach ($this->joins as $join) foreach ($this->joins as $join)
$sqlStatement .= " " . $join['type']->toString() . " NATURAL JOIN " . $join['table']; $query .= " " . $join['type']->toString() . " NATURAL JOIN " . $join['table'];
// Append the WHERE clause if conditions exist. // Append the WHERE clause if conditions exist.
if ($this->isConditioned()) { if ($this->isConditioned()) {
$whereClause = $this->combineConditions(); $whereClause = $this->combineConditions();
$sqlStatement .= " WHERE $whereClause"; $query .= " WHERE $whereClause";
} }
// Append the ORDER BY clause if ordering is set. // Append the ORDER BY clause if ordering is set.
if ($this->isOrdered()) { if ($this->isOrdered()) {
$orderClause = $this->orderBy['name'] . ' ' . $this->orderBy['order']; $orderClause = $this->orderBy['name'] . ' ' . $this->orderBy['order'];
$sqlStatement .= " ORDER BY $orderClause"; $query .= " ORDER BY $orderClause";
} }
return esc_sql($sqlStatement); return $query;
} }
public function aggregate(string $col, string $alias, Aggregation $func): Query { public function aggregate(string $col, string $alias, Aggregation $func): Query {

214
Table.php
View File

@ -2,165 +2,131 @@
namespace DatabaseHelper; namespace DatabaseHelper;
use DatabaseHelper\enums\Propagation; use DatabaseHelper\enums\Propagation;
use DatabaseHelper\enums\Charset;
use DatabaseHelper\enums\Collation;
use DatabaseHelper\enums\Type; use DatabaseHelper\enums\Type;
use DatabaseHelper\enums\Engine;
use InvalidArgumentException; use InvalidArgumentException;
class Migration class Table
{ {
protected Schema $schema; protected Schema $table;
protected array $columnsToAdd = [];
protected array $columnsToModify = [];
protected array $columnsToDrop = [];
protected ?array $primaryKey = null;
protected array $foreignKeysToAdd = [];
protected array $foreignKeysToDrop = [];
public function __construct(Schema $table) { public function __construct(string $tableName) {
$this->schema = $table->copy(); Database::standardizeTableNames($tableName);
$this->table = new Schema($tableName);
} }
public function column(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration { public function column(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Table {
if ($this->schema->existsColumn($name)) $this->table->columns[$name] = [
throw new InvalidArgumentException("Column '$name' already exists."); 'name' => $name,
$this->schema->columns[$name] = [ 'default' => $default,
'name' => $name, 'type' => $type,
'type' => $type,
'default' => $default,
'isNullable' => $isNullable, 'isNullable' => $isNullable,
'isUnique' => $isUnique 'isUnique' => $isUnique
]; ];
$this->columnsToAdd[] = $name;
return $this; return $this;
} }
public function modify(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration { public function primary(string $name, Type $type, bool $autoInc = false): Table {
$this->schema->requireColumn($name); if(isset($this->primaryKey))
$this->schema->columns[$name] = [ throw new InvalidArgumentException('Primary column already exists.');
'name' => $name,
'type' => $type, $this->table->primaryKey = [
'default' => $default, 'name' => $name,
'isNullable' => $isNullable, 'autoInc' => $autoInc
'isUnique' => $isUnique
]; ];
$this->columnsToModify[] = $name;
return $this; return $this->column($name, $type);
} }
public function delete(string $name): Migration { public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Table {
$this->schema->requireColumn($name);
unset($this->schema->columns[$name]);
$this->columnsToDrop[] = $name;
return $this;
}
public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Migration {
$name = $foreignTable->primaryKey(); $name = $foreignTable->primaryKey();
if ($this->schema->existsColumn($name)) if(isset($this->columns[$name]))
throw new InvalidArgumentException("Column '$name' already exists."); throw new InvalidArgumentException('Column name already exists.');
$this->schema->foreignKeys[$name] = [ $this->table->foreignKeys[$name] = [
'name' => $name, 'name' => $name,
'table' => $foreignTable->name, 'table' => $foreignTable->name,
'onDelete' => $onDelete, 'onDelete' => $onDelete,
'onUpdate' => $onUpdate 'onUpdate' => $onUpdate
]; ];
$this->schema->columns[$name] = $foreignTable->columns[$name]; $this->table->columns[$name] = $foreignTable->columns[$name];
$this->foreignKeysToAdd[] = $name;
return $this; return $this;
} }
public function dereference(Schema $foreignTable): Migration { public function engine(Engine $engine): Table {
$name = $foreignTable->primaryKey(); $this->table->engine = $engine;
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;
return $this; return $this;
} }
public function charset(Charset $charset): Table {
$this->table->charset = $charset;
return $this;
}
public function collation(Collation $collation): Table {
$this->table->collation = $collation;
return $this;
}
/**
* Generates the SQL statement.
* @return string SQL query.
* @throws InvalidArgumentException
*/
public function toSql(): string { public function toSql(): string {
global $wpdb; $primaryKey = $this->table->primaryKey();
// 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. $clause = "CREATE TABLE `{$this->table->name}` (\n";
foreach ($this->columnsToAdd as $columnName) { $clause .= " PRIMARY KEY (`$primaryKey`),\n";
$col = $this->schema->columns[$columnName];
$clause = "ADD COLUMN `{$col['name']}` " . $col['type']->toString(); foreach ($this->table->columns as $col) {
if (!$col['isNullable']) { if ($col['name'] !== $primaryKey) {
$clause .= " NOT NULL"; $clause .= " `{$col['name']}` {$col['type']->toString()}";
$clause .= !$col['isNullable'] ? ' NOT NULL' : '';
$clause .= $col['isUnique'] ? ' UNIQUE' : '';
if (!is_null($col['default'])) {
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
$clause .= " DEFAULT $default";
}
$clause .= ",\n";
} }
if ($col['isUnique']) {
$clause .= " UNIQUE";
}
if ($col['default'] !== null) {
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
$clause .= " DEFAULT $default";
}
$clauses[] = $clause;
} }
// Process modified columns. // Add foreign keys
foreach ($this->columnsToModify as $columnName) { foreach ($this->table->foreignKeys as $key) {
$col = $this->schema->columns[$columnName]; $clause .= " FOREIGN KEY (`{$key['name']}`)";
$clause = "MODIFY COLUMN `{$col['name']}` " . $col['type']->toString(); $clause .= " REFERENCES `{$key['table']}` (`{$key['name']}`)";
if (!$col['isNullable']) { $clause .= " ON DELETE {$key['onDelete']->toString()}";
$clause .= " NOT NULL"; $clause .= " ON UPDATE {$key['onUpdate']->toString()},\n";
}
if ($col['isUnique']) {
$clause .= " UNIQUE";
}
if ($col['default'] !== null) {
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
$clause .= " DEFAULT $default";
}
$clauses[] = $clause;
} }
// Process dropped columns. // Close the SQL string and add constraints
foreach ($this->columnsToDrop as $columnName) { $clause = rtrim($clause, ",\n") . "\n) ";
$clauses[] = "DROP COLUMN `{$columnName}`"; $clause .= "ENGINE={$this->table->engine->toString()} ";
} $clause .= "CHARSET={$this->table->charset->toString()} ";
$clause -= "COLLATE={$this->table->collation->toString()};";
// Process foreign keys to add. return $clause;
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;
}
// 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}`";
}
if (empty($clauses)) {
throw new InvalidArgumentException("No migration operations to perform.");
}
$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 { public function create(): Schema {
global $wpdb; global $wpdb;
$sql = $this->toSql();
$wpdb->query($sql); if (empty($this->primaryKey))
return $this->schema; throw new InvalidArgumentException('A primary key must be defined.');
$table_name = $wpdb->prefix . $this->table->name;
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
$sql = $this->toSql();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
return $this->table;
} }
} }

View File

@ -48,12 +48,12 @@ class Update
$setClause = implode(", ", $setParts); $setClause = implode(", ", $setParts);
// FINAL SQL STATEMENT // FINAL SQL STATEMENT
$sqlStatement = "UPDATE $table SET $setClause"; $update = "UPDATE $table SET $setClause";
if($this->isConditioned()) { if($this->isConditioned()) {
$whereClause = $this->combineConditions(); $whereClause = $this->combineConditions();
$sqlStatement .= " WHERE $whereClause"; $update .= " WHERE $whereClause";
} }
return esc_sql($sqlStatement); return $update;
} }
/** /**