From fe9661d611cfec5a0c55f30c0f8a43cbef7fda98 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen <mail@jnloos.de> Date: Wed, 12 Feb 2025 16:55:03 +0100 Subject: [PATCH] finish migration class --- Deletion.php | 2 +- Migration.php | 112 ++++++++++++++++++++++---- Query.php | 10 +-- Table.php | 214 +++++++++++++++++++++----------------------------- Update.php | 6 +- 5 files changed, 196 insertions(+), 148 deletions(-) diff --git a/Deletion.php b/Deletion.php index 97f3c38..4d6a622 100644 --- a/Deletion.php +++ b/Deletion.php @@ -26,7 +26,7 @@ class Deletion if (!$this->isConditioned()) throw new InvalidArgumentException("Deletions need to be conditioned."); - return esc_sql("DELETE FROM $table WHERE $whereClause"); + return "DELETE FROM $table WHERE $whereClause"; } /** diff --git a/Migration.php b/Migration.php index b5bbb61..ea327f1 100644 --- a/Migration.php +++ b/Migration.php @@ -11,7 +11,6 @@ class Migration protected array $columnsToAdd = []; protected array $columnsToModify = []; protected array $columnsToDrop = []; - protected array $primaryKey = null; protected array $foreignKeysToAdd = []; protected array $foreignKeysToDrop = []; @@ -19,34 +18,40 @@ class Migration $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)) throw new InvalidArgumentException("Column '$name' already exists."); + $this->schema->columns[$name] = [ - 'name' => $name, - 'type' => $type, - 'default' => $default, + 'name' => $name, + 'type' => $type, + 'default' => $default, 'isNullable' => $isNullable, - 'isUnique' => $isUnique + 'isUnique' => $isUnique ]; + $this->columnsToAdd[] = $name; return $this; } public function modify(string $name, Type $type, mixed $default = null, bool $isNullable = false, bool $isUnique = false): Migration { $this->schema->requireColumn($name); + if (isset($this->schema->foreignKeys[$name])) + throw new InvalidArgumentException('Referencing columns cannot be modified.'); + $this->schema->columns[$name] = [ - 'name' => $name, - 'type' => $type, - 'default' => $default, + 'name' => $name, + 'type' => $type, + 'default' => $default, 'isNullable' => $isNullable, - 'isUnique' => $isUnique + 'isUnique' => $isUnique ]; + $this->columnsToModify[] = $name; return $this; } - public function delete(string $name): Migration { + public function drop(string $name): Migration { $this->schema->requireColumn($name); unset($this->schema->columns[$name]); $this->columnsToDrop[] = $name; @@ -54,20 +59,97 @@ class 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 { + $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 drop(): null { 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 { diff --git a/Query.php b/Query.php index b5a5a97..ef9d76a 100644 --- a/Query.php +++ b/Query.php @@ -75,26 +75,26 @@ class Query // Build the SELECT clause. $columns = implode(", ", $selectColumns); $primaryTable = $this->schema->name; - $sqlStatement = "SELECT $columns FROM $primaryTable"; + $query = "SELECT $columns FROM $primaryTable"; // Append join clauses, if any. if ($this->isJoined()) 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. if ($this->isConditioned()) { $whereClause = $this->combineConditions(); - $sqlStatement .= " WHERE $whereClause"; + $query .= " WHERE $whereClause"; } // Append the ORDER BY clause if ordering is set. if ($this->isOrdered()) { $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 { diff --git a/Table.php b/Table.php index 06e957d..4892a8d 100644 --- a/Table.php +++ b/Table.php @@ -2,165 +2,131 @@ 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 Migration +class Table { - protected Schema $schema; - protected array $columnsToAdd = []; - protected array $columnsToModify = []; - protected array $columnsToDrop = []; - protected ?array $primaryKey = null; - protected array $foreignKeysToAdd = []; - protected array $foreignKeysToDrop = []; + protected Schema $table; - public function __construct(Schema $table) { - $this->schema = $table->copy(); + public function __construct(string $tableName) { + 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 { - if ($this->schema->existsColumn($name)) - throw new InvalidArgumentException("Column '$name' already exists."); - $this->schema->columns[$name] = [ - 'name' => $name, - 'type' => $type, - 'default' => $default, + 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, 'isNullable' => $isNullable, - 'isUnique' => $isUnique + 'isUnique' => $isUnique ]; - $this->columnsToAdd[] = $name; + return $this; } - 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 + 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 ]; - $this->columnsToModify[] = $name; - return $this; + + return $this->column($name, $type); } - public function delete(string $name): Migration { - $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 { + public function reference(Schema $foreignTable, Propagation $onDelete = Propagation::CASCADE, Propagation $onUpdate = Propagation::CASCADE): Table { $name = $foreignTable->primaryKey(); - if ($this->schema->existsColumn($name)) - throw new InvalidArgumentException("Column '$name' already exists."); + if(isset($this->columns[$name])) + throw new InvalidArgumentException('Column name already exists.'); - $this->schema->foreignKeys[$name] = [ - 'name' => $name, - 'table' => $foreignTable->name, + $this->table->foreignKeys[$name] = [ + 'name' => $name, + 'table' => $foreignTable->name, 'onDelete' => $onDelete, 'onUpdate' => $onUpdate ]; - $this->schema->columns[$name] = $foreignTable->columns[$name]; - $this->foreignKeysToAdd[] = $name; + $this->table->columns[$name] = $foreignTable->columns[$name]; return $this; } - 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; + public function engine(Engine $engine): Table { + $this->table->engine = $engine; 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 { - 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 = []; + $primaryKey = $this->table->primaryKey(); - // 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"; + $clause = "CREATE TABLE `{$this->table->name}` (\n"; + $clause .= " PRIMARY KEY (`$primaryKey`),\n"; + + foreach ($this->table->columns as $col) { + if ($col['name'] !== $primaryKey) { + $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. - 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; + // Add foreign keys + foreach ($this->table->foreignKeys as $key) { + $clause .= " FOREIGN KEY (`{$key['name']}`)"; + $clause .= " REFERENCES `{$key['table']}` (`{$key['name']}`)"; + $clause .= " ON DELETE {$key['onDelete']->toString()}"; + $clause .= " ON UPDATE {$key['onUpdate']->toString()},\n"; } - // 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 drop(): null { - global $wpdb; - $tableName = $wpdb->prefix . $this->schema->name; - $sql = "DROP TABLE IF EXISTS `{$tableName}`;"; - $wpdb->query(esc_sql($sql)); - return null; + // Close the SQL string and add constraints + $clause = rtrim($clause, ",\n") . "\n) "; + $clause .= "ENGINE={$this->table->engine->toString()} "; + $clause .= "CHARSET={$this->table->charset->toString()} "; + $clause -= "COLLATE={$this->table->collation->toString()};"; + return $clause; } - public function migrate(): Schema { + public function create(): Schema { global $wpdb; - $sql = $this->toSql(); - $wpdb->query($sql); - return $this->schema; + + 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; } -} +} \ No newline at end of file diff --git a/Update.php b/Update.php index 9e88c55..05496c0 100644 --- a/Update.php +++ b/Update.php @@ -48,12 +48,12 @@ class Update $setClause = implode(", ", $setParts); // FINAL SQL STATEMENT - $sqlStatement = "UPDATE $table SET $setClause"; + $update = "UPDATE $table SET $setClause"; if($this->isConditioned()) { $whereClause = $this->combineConditions(); - $sqlStatement .= " WHERE $whereClause"; + $update .= " WHERE $whereClause"; } - return esc_sql($sqlStatement); + return $update; } /**