improve migration class
This commit is contained in:
parent
29703314a3
commit
c4a7025978
@ -7,7 +7,7 @@ use InvalidArgumentException;
|
||||
|
||||
class Migration
|
||||
{
|
||||
protected Schema $table;
|
||||
protected Schema $schema;
|
||||
protected array $columnsToAdd = [];
|
||||
protected array $columnsToModify = [];
|
||||
protected array $columnsToDrop = [];
|
||||
@ -16,11 +16,13 @@ class Migration
|
||||
protected array $foreignKeysToDrop = [];
|
||||
|
||||
public function __construct(Schema $table) {
|
||||
$this->table = $table->copy();
|
||||
$this->schema = $table->copy();
|
||||
}
|
||||
|
||||
public function addColumn(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration {
|
||||
$this->table->columns[$name] = [
|
||||
public function add(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,
|
||||
@ -31,11 +33,9 @@ class Migration
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function modifyColumn(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration {
|
||||
if (!isset($this->table->columns[$name])) {
|
||||
throw new InvalidArgumentException("Column $name does not exist.");
|
||||
}
|
||||
$this->table->columns[$name] = [
|
||||
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,
|
||||
@ -46,87 +46,27 @@ class Migration
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dropColumn(string $name): Migration {
|
||||
if (!isset($this->table->columns[$name])) {
|
||||
throw new InvalidArgumentException("Column $name does not exist.");
|
||||
}
|
||||
unset($this->table->columns[$name]);
|
||||
public function delete(string $name): Migration {
|
||||
$this->schema->requireColumn($name);
|
||||
unset($this->schema->columns[$name]);
|
||||
$this->columnsToDrop[] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function changePrimaryKey(string $name, bool $autoInc = false): Migration {
|
||||
if (!isset($this->table->columns[$name])) {
|
||||
throw new InvalidArgumentException("Column $name does not exist.");
|
||||
}
|
||||
$this->table->primaryKey = [
|
||||
'name' => $name,
|
||||
'autoInc' => $autoInc
|
||||
];
|
||||
$this->primaryKey = $name;
|
||||
return $this;
|
||||
public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Migration {
|
||||
|
||||
}
|
||||
|
||||
public function addForeignKey(string $column, string $referencedTable, string $referencedColumn, Propagation $onDelete, Propagation $onUpdate): Migration {
|
||||
if (!isset($this->table->columns[$column])) {
|
||||
throw new InvalidArgumentException("Column $column does not exist.");
|
||||
}
|
||||
$this->table->foreignKeys[$column] = [
|
||||
'referencedTable' => $referencedTable,
|
||||
'referencedColumn' => $referencedColumn,
|
||||
'onDelete' => $onDelete,
|
||||
'onUpdate' => $onUpdate
|
||||
];
|
||||
$this->foreignKeysToAdd[] = $column;
|
||||
return $this;
|
||||
}
|
||||
public function dereference(Schema $foreignTable): Migration {
|
||||
|
||||
public function dropForeignKey(string $name): Migration {
|
||||
if (!isset($this->table->foreignKeys[$name])) {
|
||||
throw new InvalidArgumentException("Foreign key $name does not exist.");
|
||||
}
|
||||
unset($this->table->foreignKeys[$name]);
|
||||
$this->foreignKeysToDrop[] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toSql(): string {
|
||||
$sql = "ALTER TABLE `{$this->table->name}` ";
|
||||
$statements = [];
|
||||
|
||||
foreach ($this->columnsToAdd as $name) {
|
||||
$col = $this->table->columns[$name];
|
||||
$statements[] = "ADD COLUMN `{$col['name']}` {$col['type']->toString()}" .
|
||||
($col['isNullable'] ? "" : " NOT NULL") .
|
||||
($col['isUnique'] ? " UNIQUE" : "") .
|
||||
($col['default'] !== null ? " DEFAULT " . (is_string($col['default']) ? "'{$col['default']}'" : $col['default']) : "");
|
||||
}
|
||||
|
||||
foreach ($this->columnsToModify as $name) {
|
||||
$col = $this->table->columns[$name];
|
||||
$statements[] = "MODIFY COLUMN `{$col['name']}` {$col['type']->toString()}" .
|
||||
($col['isNullable'] ? "" : " NOT NULL") .
|
||||
($col['isUnique'] ? " UNIQUE" : "") .
|
||||
($col['default'] !== null ? " DEFAULT " . (is_string($col['default']) ? "'{$col['default']}'" : $col['default']) : "");
|
||||
}
|
||||
|
||||
foreach ($this->columnsToDrop as $name) {
|
||||
$statements[] = "DROP COLUMN `$name`";
|
||||
}
|
||||
|
||||
foreach ($this->foreignKeysToDrop as $name) {
|
||||
$statements[] = "DROP FOREIGN KEY `$name`";
|
||||
}
|
||||
|
||||
foreach ($this->foreignKeysToAdd as $column) {
|
||||
$fk = $this->table->foreignKeys[$column];
|
||||
$statements[] = "ADD CONSTRAINT `fk_{$column}` FOREIGN KEY (`$column`) REFERENCES `{$fk['referencedTable']}` (`{$fk['referencedColumn']}`) ON DELETE {$fk['onDelete']->toString()} ON UPDATE {$fk['onUpdate']->toString()}";
|
||||
}
|
||||
|
||||
return $sql . implode(", ", $statements) . ";";
|
||||
}
|
||||
|
||||
public function drop(): null {
|
||||
global $wpdb;
|
||||
|
||||
}
|
||||
|
||||
@ -134,6 +74,6 @@ class Migration
|
||||
global $wpdb;
|
||||
$sql = $this->toSql();
|
||||
$wpdb->query($sql);
|
||||
return $this->table;
|
||||
return $this->schema;
|
||||
}
|
||||
}
|
||||
|
55
Query.php
55
Query.php
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace DatabaseHelper;
|
||||
|
||||
use DatabaseHelper\enums\Aggregation;
|
||||
use DatabaseHelper\enums\Join;
|
||||
use DatabaseHelper\enums\Order;
|
||||
use http\Exception\InvalidArgumentException;
|
||||
@ -12,6 +13,7 @@ class Query
|
||||
|
||||
protected Schema $schema;
|
||||
protected array $columns = ['*'];
|
||||
protected array $aggregations = [];
|
||||
protected array $joins = [];
|
||||
public array $orderBy;
|
||||
|
||||
@ -36,7 +38,7 @@ class Query
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function join(Schema $other, Join $join): Query {
|
||||
public function join(Join $join, Schema $other): Query {
|
||||
$foreignKey = null;
|
||||
if($this->schema->existsReference($other))
|
||||
$foreignKey = $this->schema->foreignKeys[$other->name];
|
||||
@ -65,17 +67,20 @@ class Query
|
||||
}
|
||||
|
||||
public function toSql(): string {
|
||||
// Merge any aggregations with the standard columns.
|
||||
$selectColumns = $this->columns;
|
||||
if ($this->hasAggregations())
|
||||
$selectColumns = array_merge($selectColumns, $this->aggregations);
|
||||
|
||||
// Build the SELECT clause.
|
||||
$columns = implode(", ", $this->columns);
|
||||
$columns = implode(", ", $selectColumns);
|
||||
$primaryTable = $this->schema->name;
|
||||
$sqlStatement = "SELECT $columns FROM $primaryTable";
|
||||
|
||||
// Append join clauses, if any.
|
||||
if ($this->isJoined()) {
|
||||
foreach ($this->joins as $join) {
|
||||
if ($this->isJoined())
|
||||
foreach ($this->joins as $join)
|
||||
$sqlStatement .= " " . $join['type']->toString() . " NATURAL JOIN " . $join['table'];
|
||||
}
|
||||
}
|
||||
|
||||
// Append the WHERE clause if conditions exist.
|
||||
if ($this->isConditioned()) {
|
||||
@ -92,18 +97,46 @@ class Query
|
||||
return esc_sql($sqlStatement);
|
||||
}
|
||||
|
||||
public function query(): array {
|
||||
public function aggregate(string $col, string $alias, Aggregation $func): Query {
|
||||
if ($col != '*')
|
||||
$this->schema->requireColumn($col);
|
||||
$this->aggregations[] = strtoupper($func->toString()) . "($col) AS $alias";
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasAggregations(): bool {
|
||||
return !empty($this->aggregations);
|
||||
}
|
||||
|
||||
public function query(): mixed {
|
||||
global $wpdb;
|
||||
$query = $this->toSql();
|
||||
$results = $wpdb->get_results($query, ARRAY_A);
|
||||
return $this->castResults($results);
|
||||
return $this->formatResults($results);
|
||||
}
|
||||
|
||||
protected function castResults(array $results): array {
|
||||
foreach ($results as &$row)
|
||||
protected function formatResults(array $results) {
|
||||
$formatted = [];
|
||||
|
||||
foreach ($results as $row) {
|
||||
// Apply type casting to each column in each row.
|
||||
foreach ($row as $column => &$value)
|
||||
if (isset($this->columnTypes[$column]))
|
||||
$value = $this->columnTypes[$column]->valCast($value);
|
||||
return $results;
|
||||
// Use the primary key for row indexing
|
||||
$primaryKey = $this->schema->primaryKey();
|
||||
$formatted[$row[$primaryKey]] = $row;
|
||||
}
|
||||
|
||||
if (count($formatted) === 1) {
|
||||
// Unpack single row results
|
||||
$row = array_shift($formatted);
|
||||
if (count($row) === 1)
|
||||
// Unpack single column results
|
||||
return array_shift($row);
|
||||
return $row;
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
}
|
||||
|
11
Schema.php
11
Schema.php
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace DatabaseHelper;
|
||||
|
||||
use DatabaseHelper\enums\Aggregation;
|
||||
use DatabaseHelper\enums\Charset;
|
||||
use DatabaseHelper\enums\Collation;
|
||||
use DatabaseHelper\enums\Engine;
|
||||
@ -38,6 +39,12 @@ class Schema
|
||||
return false;
|
||||
}
|
||||
|
||||
public function countEntries(): int {
|
||||
return Database::makeQuery($this)
|
||||
->aggregate('*', 'count', Aggregation::COUNT)
|
||||
->query();
|
||||
}
|
||||
|
||||
public function columnType(string $col) {
|
||||
return $this->columns[$col]['type'];
|
||||
}
|
||||
@ -46,10 +53,6 @@ class Schema
|
||||
return $this->columns['primary']['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a deep copy of this instance.
|
||||
* @return Schema
|
||||
*/
|
||||
public function copy(): Schema {
|
||||
$copy = new Schema($this->name);
|
||||
$copy->columns = genericDeepCopy($this->columns);
|
||||
|
211
Table.php
211
Table.php
@ -2,130 +2,165 @@
|
||||
namespace DatabaseHelper;
|
||||
|
||||
use DatabaseHelper\enums\Propagation;
|
||||
use DatabaseHelper\enums\Charset;
|
||||
use DatabaseHelper\enums\Collation;
|
||||
use DatabaseHelper\enums\Type;
|
||||
use DatabaseHelper\enums\Engine;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Table
|
||||
class Migration
|
||||
{
|
||||
protected Schema $table;
|
||||
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(string $tableName) {
|
||||
Database::standardizeTableNames($tableName);
|
||||
$this->table = new Schema($tableName);
|
||||
public function __construct(Schema $table) {
|
||||
$this->schema = $table->copy();
|
||||
}
|
||||
|
||||
public function column(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Table {
|
||||
$this->table->columns[$name] = [
|
||||
'name' => $name,
|
||||
'default' => $default,
|
||||
'type' => $type,
|
||||
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,
|
||||
'isNullable' => $isNullable,
|
||||
'isUnique' => $isUnique
|
||||
'isUnique' => $isUnique
|
||||
];
|
||||
|
||||
$this->columnsToAdd[] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function primary(string $name, Type $type, bool $autoInc = false): Table {
|
||||
if(isset($this->primaryKey))
|
||||
throw new InvalidArgumentException('Primary column already exists.');
|
||||
|
||||
$this->table->primaryKey = [
|
||||
'name' => $name,
|
||||
'autoInc' => $autoInc
|
||||
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
|
||||
];
|
||||
|
||||
return $this->column($name, $type);
|
||||
$this->columnsToModify[] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Table {
|
||||
$name = $foreignTable->primaryKey();
|
||||
if(isset($this->columns[$name]))
|
||||
throw new InvalidArgumentException('Column name already exists.');
|
||||
public function delete(string $name): Migration {
|
||||
$this->schema->requireColumn($name);
|
||||
unset($this->schema->columns[$name]);
|
||||
$this->columnsToDrop[] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->table->foreignKeys[$name] = [
|
||||
'name' => $name,
|
||||
'table' => $foreignTable->name,
|
||||
public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Migration {
|
||||
$name = $foreignTable->primaryKey();
|
||||
if ($this->schema->existsColumn($name))
|
||||
throw new InvalidArgumentException("Column '$name' already exists.");
|
||||
|
||||
$this->schema->foreignKeys[$name] = [
|
||||
'name' => $name,
|
||||
'table' => $foreignTable->name,
|
||||
'onDelete' => $onDelete,
|
||||
'onUpdate' => $onUpdate
|
||||
];
|
||||
|
||||
$this->table->columns[$name] = $foreignTable->columns[$name];
|
||||
$this->schema->columns[$name] = $foreignTable->columns[$name];
|
||||
$this->foreignKeysToAdd[] = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function engine(Engine $engine): Table {
|
||||
$this->table->engine = $engine;
|
||||
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;
|
||||
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 {
|
||||
$primaryKey = $this->table->primaryKey();
|
||||
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 = [];
|
||||
|
||||
$sql = "CREATE TABLE `{$this->table->name}` (\n";
|
||||
$sql .= " PRIMARY KEY (`$primaryKey`),\n";
|
||||
|
||||
foreach ($this->table->columns as $col) {
|
||||
if ($col['name'] !== $primaryKey) {
|
||||
$sql .= " `{$col['name']}` {$col['type']->toString()}";
|
||||
// Handle nulls, uniqueness, and defaults
|
||||
if (!$col['isNullable'])
|
||||
$sql .= " NOT NULL";
|
||||
if ($col['isUnique'])
|
||||
$sql .= " UNIQUE";
|
||||
if ($col['default'] !== null) {
|
||||
$default = is_string($col['default']) ? "'{$col['default']}'" : $col['default'];
|
||||
$sql .= " DEFAULT $default";
|
||||
}
|
||||
|
||||
$sql .= ",\n";
|
||||
// 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";
|
||||
}
|
||||
$clauses[] = $clause;
|
||||
}
|
||||
|
||||
// Add secondary constraints if present
|
||||
foreach ($this->table->foreignKeys as $key) {
|
||||
$sql .= " FOREIGN KEY (`{$key['name']}`) REFERENCES `{$key['table']}` (`{$key['name']}`)";
|
||||
$sql .= " ON DELETE {$key['onDelete']->toString()} ON UPDATE {$key['onUpdate']->toString()},\n";
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Close the SQL string and add constraints
|
||||
$sql = rtrim($sql, ",\n") . "\n) ";
|
||||
$sql .= "ENGINE={$this->table->engine->toString()} ";
|
||||
$sql .= "CHARSET={$this->table->charset->toString()} ";
|
||||
$sql -= "COLLATE={$this->table->collation->toString()};";
|
||||
// Process dropped columns.
|
||||
foreach ($this->columnsToDrop as $columnName) {
|
||||
$clauses[] = "DROP COLUMN `{$columnName}`";
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 create(): Schema {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (empty($this->primaryKey))
|
||||
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;
|
||||
public function migrate(): Schema {
|
||||
global $wpdb;
|
||||
$sql = $this->toSql();
|
||||
$wpdb->query($sql);
|
||||
return $this->schema;
|
||||
}
|
||||
}
|
||||
|
21
enums/Aggregation.php
Normal file
21
enums/Aggregation.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace DatabaseHelper\enums;
|
||||
|
||||
enum Aggregation
|
||||
{
|
||||
case COUNT;
|
||||
case SUM;
|
||||
case AVG;
|
||||
case MIN;
|
||||
case MAX;
|
||||
|
||||
public function toString(): string {
|
||||
return match ($this) {
|
||||
self::COUNT => 'COUNT',
|
||||
self::SUM => 'SUM',
|
||||
self::AVG => 'AVG',
|
||||
self::MIN => 'MIN',
|
||||
self::MAX => 'MAX',
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user