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;
     }
 
     /**