diff --git a/dev/config.php b/dev/config.php
index 2d5a834..438981e 100644
--- a/dev/config.php
+++ b/dev/config.php
@@ -3,8 +3,6 @@ declare(strict_types = 1);
 
 define('DEBUG', true);
 
-define('INSTALLED_MIGRATION_VERSION', TRACKER_MIGRATION_VERSION);
-
 define('SYS_ENABLE_REGISTRATION', true);
 define('BASE_URL', 'http://localhost');
 
diff --git a/dev/version.php b/dev/version.php
new file mode 100644
index 0000000..3c5c494
--- /dev/null
+++ b/dev/version.php
@@ -0,0 +1,6 @@
+<?php
+declare(strict_types = 1);
+
+define('MIGRATION_VERSION', TRACKER_MIGRATION_VERSION);
+define('MIGRATION_TASK', 0);
+?>
diff --git a/src/Configuration/SystemConfig.php b/src/Configuration/SystemConfig.php
index c81507e..71c5b7e 100644
--- a/src/Configuration/SystemConfig.php
+++ b/src/Configuration/SystemConfig.php
@@ -57,7 +57,7 @@ final class SystemConfig{
     $validator->validate();
   }
   
-  public function generate(int $migration_version = TRACKER_MIGRATION_VERSION): string{
+  public function generate(): string{
     $sys_enable_registration = $this->sys_enable_registration ? 'true' : 'false';
     $base_url = addcslashes($this->base_url, '\'\\');
     $db_name = addcslashes($this->db_name, '\'\\');
@@ -70,8 +70,6 @@ final class SystemConfig{
 <?php
 declare(strict_types = 1);
 
-define('INSTALLED_MIGRATION_VERSION', $migration_version);
-
 define('SYS_ENABLE_REGISTRATION', $sys_enable_registration);
 define('BASE_URL', '$base_url');
 
@@ -85,6 +83,10 @@ PHP;
     
     return $contents;
   }
+  
+  public function write(string $file): bool{
+    return file_put_contents($file, $this->generate(), LOCK_EX) !== false;
+  }
 }
 
 ?>
diff --git a/src/Configuration/VersionFile.php b/src/Configuration/VersionFile.php
new file mode 100644
index 0000000..4438997
--- /dev/null
+++ b/src/Configuration/VersionFile.php
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+
+namespace Configuration;
+
+final class VersionFile{
+  private int $migration_version;
+  private int $migration_task;
+  
+  public function __construct(int $migration_version, int $migration_task){
+    $this->migration_version = $migration_version;
+    $this->migration_task = $migration_task;
+  }
+  
+  public function generate(): string{
+    /** @noinspection ALL */
+    $contents = <<<PHP
+<?php
+declare(strict_types = 1);
+
+define('MIGRATION_VERSION', $this->migration_version);
+define('MIGRATION_TASK', $this->migration_task);
+?>
+PHP;
+    
+    return $contents;
+  }
+  
+  public function writeSafe(string $target_file, string $tmp_file): bool{
+    return file_put_contents($tmp_file, $this->generate(), LOCK_EX) && rename($tmp_file, $target_file);
+  }
+}
+
+?>
diff --git a/src/Pages/Models/Root/SettingsGeneralModel.php b/src/Pages/Models/Root/SettingsGeneralModel.php
index 54c773b..fc27085 100644
--- a/src/Pages/Models/Root/SettingsGeneralModel.php
+++ b/src/Pages/Models/Root/SettingsGeneralModel.php
@@ -120,7 +120,7 @@ HTML;
       return false;
     }
     
-    if (!file_put_contents(CONFIG_FILE, $config->generate(), LOCK_EX)){
+    if (!$config->write(CONFIG_FILE)){
       $this->form->addMessage(FormComponent::MESSAGE_ERROR, Text::blocked('Error updating \'config.php\'.'));
       return false;
     }
diff --git a/src/Update/AbstractMigrationProcess.php b/src/Update/AbstractMigrationProcess.php
new file mode 100644
index 0000000..346fa9e
--- /dev/null
+++ b/src/Update/AbstractMigrationProcess.php
@@ -0,0 +1,19 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update;
+
+use Update\Tasks\SqlTask;
+
+abstract class AbstractMigrationProcess{
+  protected static final function sql(string $sql): SqlTask{
+    return new SqlTask($sql);
+  }
+  
+  /**
+   * @return AbstractMigrationTask[]
+   */
+  public abstract function getTasks(): array;
+}
+
+?>
diff --git a/src/Update/AbstractMigrationTask.php b/src/Update/AbstractMigrationTask.php
new file mode 100644
index 0000000..f5d9313
--- /dev/null
+++ b/src/Update/AbstractMigrationTask.php
@@ -0,0 +1,18 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update;
+
+use PDO;
+
+abstract class AbstractMigrationTask{
+  public function prepare(PDO $db): void{
+  }
+  
+  public abstract function execute(PDO $db): void;
+  
+  public function finalize(PDO $db): void{
+  }
+}
+
+?>
diff --git a/src/Update/MigrationManager.php b/src/Update/MigrationManager.php
new file mode 100644
index 0000000..3903bef
--- /dev/null
+++ b/src/Update/MigrationManager.php
@@ -0,0 +1,53 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update;
+
+use Configuration\VersionFile;
+use Exception;
+
+final class MigrationManager{
+  private int $current_version;
+  private int $current_task;
+  
+  public function __construct(int $current_version, int $current_task){
+    $this->current_version = $current_version;
+    $this->current_task = $current_task;
+  }
+  
+  public function getCurrentVersion(): int{
+    return $this->current_version;
+  }
+  
+  public function getCurrentTask(): int{
+    return $this->current_task;
+  }
+  
+  /**
+   * @throws Exception
+   */
+  public function finishVersion(): void{
+    ++$this->current_version;
+    $this->current_task = 0;
+    $this->writeFile();
+  }
+  
+  /**
+   * @throws Exception
+   */
+  public function finishTask(): void{
+    ++$this->current_task;
+    $this->writeFile();
+  }
+  
+  /**
+   * @throws Exception
+   */
+  private function writeFile(): void{
+    if (!(new VersionFile($this->current_version, $this->current_task))->writeSafe(VERSION_FILE, VERSION_TMP_FILE)){
+      throw new Exception('Error updating version file (migration '.$this->current_version.', task '.$this->current_task.').');
+    }
+  }
+}
+
+?>
diff --git a/src/Update/Migrations/Migration6.php b/src/Update/Migrations/Migration6.php
new file mode 100644
index 0000000..18230bc
--- /dev/null
+++ b/src/Update/Migrations/Migration6.php
@@ -0,0 +1,95 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update\Migrations;
+
+use Data\UserId;
+use PDO;
+use Update\AbstractMigrationProcess;
+use Update\AbstractMigrationTask;
+use Update\Tasks\DropAllForeignKeysTask;
+
+final class Migration6 extends AbstractMigrationProcess{
+  public function getTasks(): array{
+    /** @noinspection SqlResolve, SqlWithoutWhere */
+    return [
+        new DropAllForeignKeysTask(),
+        
+        self::sql('ALTER TABLE users ADD public_id CHAR(9) NOT NULL FIRST'),
+        
+        self::sql('ALTER TABLE issues CHANGE author_id author_id_old INT NULL'),
+        self::sql('ALTER TABLE issues CHANGE assignee_id assignee_id_old INT NULL'),
+        self::sql('ALTER TABLE project_members CHANGE user_id user_id_old INT NOT NULL'),
+        self::sql('ALTER TABLE projects CHANGE owner_id owner_id_old INT NOT NULL'),
+        self::sql('ALTER TABLE project_user_settings CHANGE user_id user_id_old INT NOT NULL'),
+        self::sql('ALTER TABLE user_logins CHANGE id id_old INT NOT NULL'),
+        
+        self::sql('ALTER TABLE project_members DROP PRIMARY KEY'),
+        self::sql('ALTER TABLE project_user_settings DROP PRIMARY KEY'),
+        self::sql('ALTER TABLE user_logins DROP PRIMARY KEY'),
+        
+        self::sql('ALTER TABLE issues ADD author_id CHAR(9) NULL AFTER author_id_old'),
+        self::sql('ALTER TABLE issues ADD assignee_id CHAR(9) NULL AFTER assignee_id_old'),
+        self::sql('ALTER TABLE project_members ADD user_id CHAR(9) NOT NULL AFTER user_id_old'),
+        self::sql('ALTER TABLE projects ADD owner_id CHAR(9) NOT NULL AFTER owner_id_old'),
+        self::sql('ALTER TABLE project_user_settings ADD user_id CHAR(9) NOT NULL AFTER user_id_old'),
+        self::sql('ALTER TABLE user_logins ADD id CHAR(9) NOT NULL AFTER id_old'),
+        
+        new class extends AbstractMigrationTask{
+          public function execute(PDO $db): void{
+            $stmt = $db->query('SELECT id FROM users');
+  
+            while(($res = $stmt->fetchColumn()) !== false){
+              /** @noinspection SqlResolve */
+              $s2 = $db->prepare('UPDATE users SET public_id = ? WHERE id = ?');
+              $s2->bindValue(1, UserId::generateNew());
+              $s2->bindValue(2, (int)$res, PDO::PARAM_INT);
+              $s2->execute();
+            }
+          }
+        },
+
+        self::sql('UPDATE issues SET author_id = (SELECT u.public_id FROM users u WHERE u.id = author_id_old)'),
+        self::sql('UPDATE issues SET assignee_id = (SELECT u.public_id FROM users u WHERE u.id = assignee_id_old)'),
+        self::sql('UPDATE project_members SET user_id = (SELECT u.public_id FROM users u WHERE u.id = user_id_old)'),
+        self::sql('UPDATE projects SET owner_id = (SELECT u.public_id FROM users u WHERE u.id = owner_id_old)'),
+        self::sql('UPDATE project_user_settings SET user_id = (SELECT u.public_id FROM users u WHERE u.id = user_id_old)'),
+        self::sql('UPDATE user_logins SET id = (SELECT u.public_id FROM users u WHERE u.id = id_old)'),
+
+        self::sql('ALTER TABLE users DROP id'),
+        self::sql('ALTER TABLE users CHANGE public_id id CHAR(9) NOT NULL'),
+        self::sql('ALTER TABLE users ADD PRIMARY KEY (id)'),
+
+        self::sql('ALTER TABLE project_members ADD PRIMARY KEY (project_id, user_id)'),
+        self::sql('ALTER TABLE project_user_settings ADD PRIMARY KEY (project_id, user_id)'),
+        self::sql('ALTER TABLE user_logins ADD PRIMARY KEY (id, token)'),
+
+        self::sql('ALTER TABLE issues ADD CONSTRAINT fk__issue__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE issues ADD CONSTRAINT fk__issue__author FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE SET NULL'),
+        self::sql('ALTER TABLE issues ADD CONSTRAINT fk__issue__assignee FOREIGN KEY (`assignee_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE SET NULL'),
+        self::sql('ALTER TABLE issues ADD CONSTRAINT fk__issue__milestone FOREIGN KEY (`milestone_id`, `project_id`) REFERENCES `milestones` (`milestone_id`, `project_id`) ON UPDATE CASCADE ON DELETE RESTRICT'),
+        self::sql('ALTER TABLE issues ADD CONSTRAINT fk__issue__scale FOREIGN KEY (`scale`) REFERENCES `issue_weights` (`scale`) ON UPDATE RESTRICT ON DELETE RESTRICT'),
+        self::sql('ALTER TABLE milestones ADD CONSTRAINT fk__milestone__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE project_members ADD CONSTRAINT fk__project_member__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE project_members ADD CONSTRAINT fk__project_member__user FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE project_members ADD CONSTRAINT fk__project_member__role FOREIGN KEY (`role_id`, `project_id`) REFERENCES `project_roles` (`role_id`, `project_id`) ON UPDATE CASCADE ON DELETE RESTRICT'),
+        self::sql('ALTER TABLE project_role_permissions ADD CONSTRAINT fk__project_role_permission__role FOREIGN KEY (`role_id`, `project_id`) REFERENCES `project_roles` (`role_id`, `project_id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE project_roles ADD CONSTRAINT fk__project_role__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE '),
+        self::sql('ALTER TABLE projects ADD CONSTRAINT fk__project__owner FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE RESTRICT'),
+        self::sql('ALTER TABLE project_user_settings ADD CONSTRAINT fk__project_user_setting__user FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE project_user_settings ADD CONSTRAINT fk__project_user_setting__active_milestone FOREIGN KEY (`active_milestone`, `project_id`) REFERENCES `milestones` (`milestone_id`, `project_id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE system_role_permissions ADD CONSTRAINT fk__system_role_permission__role FOREIGN KEY (`role_id`) REFERENCES `system_roles` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE user_logins ADD CONSTRAINT fk__user_login__user FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'),
+        self::sql('ALTER TABLE users ADD CONSTRAINT fk__user__role FOREIGN KEY (`role_id`) REFERENCES `system_roles` (`id`) ON UPDATE CASCADE ON DELETE SET NULL'),
+
+        self::sql('ALTER TABLE issues DROP author_id_old'),
+        self::sql('ALTER TABLE issues DROP assignee_id_old'),
+        self::sql('ALTER TABLE project_members DROP user_id_old'),
+        self::sql('ALTER TABLE projects DROP owner_id_old'),
+        self::sql('ALTER TABLE project_user_settings DROP user_id_old'),
+        self::sql('ALTER TABLE user_logins DROP id_old')
+    ];
+  }
+}
+
+?>
diff --git a/src/Update/Tasks/DropAllForeignKeysTask.php b/src/Update/Tasks/DropAllForeignKeysTask.php
new file mode 100644
index 0000000..2276f3a
--- /dev/null
+++ b/src/Update/Tasks/DropAllForeignKeysTask.php
@@ -0,0 +1,29 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update\Tasks;
+
+use PDO;
+use Update\AbstractMigrationTask;
+
+final class DropAllForeignKeysTask extends AbstractMigrationTask{
+  public function execute(PDO $db): void{
+    $stmt = $db->prepare(<<<SQL
+SELECT DISTINCT TABLE_NAME AS tbl, CONSTRAINT_NAME AS constr
+FROM information_schema.KEY_COLUMN_USAGE
+WHERE TABLE_SCHEMA = :db_name AND REFERENCED_TABLE_SCHEMA = TABLE_SCHEMA
+SQL
+    );
+    
+    $stmt->bindValue('db_name', DB_NAME);
+    $stmt->execute();
+    $rows = $stmt->fetchAll();
+    
+    foreach($rows as $row){
+      /** @noinspection SqlResolve */
+      $db->exec('ALTER TABLE `'.$row['tbl'].'` DROP FOREIGN KEY `'.$row['constr'].'`');
+    }
+  }
+}
+
+?>
diff --git a/src/Update/Tasks/SqlTask.php b/src/Update/Tasks/SqlTask.php
new file mode 100644
index 0000000..fd13c61
--- /dev/null
+++ b/src/Update/Tasks/SqlTask.php
@@ -0,0 +1,21 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update\Tasks;
+
+use PDO;
+use Update\AbstractMigrationTask;
+
+final class SqlTask extends AbstractMigrationTask{
+  private string $sql;
+  
+  public function __construct(string $sql){
+    $this->sql = $sql;
+  }
+  
+  public function execute(PDO $db): void{
+    $db->exec($this->sql);
+  }
+}
+
+?>
diff --git a/src/Update/Tasks/SqlTransactionTask.php b/src/Update/Tasks/SqlTransactionTask.php
new file mode 100644
index 0000000..890dee8
--- /dev/null
+++ b/src/Update/Tasks/SqlTransactionTask.php
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+
+namespace Update\Tasks;
+
+use PDO;
+use Update\AbstractMigrationTask;
+
+final class SqlTransactionTask extends AbstractMigrationTask{
+  /**
+   * @var string[]
+   */
+  private array $statements;
+  
+  public function __construct(array $statements){
+    $this->statements = $statements;
+  }
+  
+  public function prepare(PDO $db): void{
+    $db->beginTransaction();
+  }
+  
+  public function execute(PDO $db): void{
+    foreach($this->statements as $sql){
+      $db->exec($sql);
+    }
+  }
+  
+  public function finalize(PDO $db): void{
+    $db->commit();
+  }
+}
+
+?>
diff --git a/src/bootstrap.php b/src/bootstrap.php
index 79ee405..762de61 100644
--- a/src/bootstrap.php
+++ b/src/bootstrap.php
@@ -1,6 +1,8 @@
 <?php
 declare(strict_types = 1);
 
+use Configuration\SystemConfig;
+use Configuration\VersionFile;
 use Logging\Log;
 use Routing\Request;
 use Routing\Router;
@@ -15,6 +17,9 @@ define('TRACKER_RESOURCE_VERSION', ''); // autogenerated
 define('CONFIG_FILE', __DIR__.'/config.php');
 define('CONFIG_BACKUP_FILE', __DIR__.'/config.old.php');
 
+define('VERSION_FILE', __DIR__.'/version.php');
+define('VERSION_TMP_FILE', __DIR__.'/version.tmp.php');
+
 setlocale(LC_ALL, 'C');
 date_default_timezone_set('UTC');
 header_remove('x-powered-by');
@@ -31,13 +36,22 @@ spl_autoload_register(static function($class){
 
 require_once 'utils.php';
 
-if (!file_exists('config.php')){
+if (!file_exists(CONFIG_FILE)){
   require_once 'install.php';
   return;
 }
 
 /** @noinspection PhpIncludeInspection */
-require_once 'config.php';
+require_once CONFIG_FILE;
+
+if (!file_exists(VERSION_FILE)){
+  $version_file = new VersionFile(constant('INSTALLED_MIGRATION_VERSION'), 0);
+  $version_file->writeSafe(VERSION_FILE, VERSION_TMP_FILE);
+  SystemConfig::fromCurrentInstallation()->write(CONFIG_FILE);
+}
+
+/** @noinspection PhpIncludeInspection */
+require_once VERSION_FILE;
 
 if (!defined('DEBUG')){
   define('DEBUG', false);
@@ -61,7 +75,7 @@ define('BASE_URL_ENC', $base_url_protocol.(new UrlString($base_url_domain_path))
 
 // Migration
 
-if (TRACKER_MIGRATION_VERSION > INSTALLED_MIGRATION_VERSION){
+if (TRACKER_MIGRATION_VERSION > MIGRATION_VERSION){
   require_once 'update.php';
 }
 
diff --git a/src/install.php b/src/install.php
index 4bc96a6..d5ce0c5 100644
--- a/src/install.php
+++ b/src/install.php
@@ -253,7 +253,7 @@ if (!empty($_POST) && $submit_action !== $action_value_conflict_cancel){
   
   // Configuration File
   
-  if (empty($errors) && !file_put_contents(__DIR__.'/config.php', $config->generate(), LOCK_EX)){
+  if (empty($errors) && !$config->write(CONFIG_FILE)){
     $errors[] = 'Error creating \'config.php\'.';
   }
   
diff --git a/src/update.php b/src/update.php
index eadd9a5..d89f616 100644
--- a/src/update.php
+++ b/src/update.php
@@ -1,369 +1,57 @@
 <?php
 declare(strict_types = 1);
 
-use Configuration\SystemConfig;
-use Data\UserId;
 use Database\DB;
 use Logging\Log;
+use Update\AbstractMigrationProcess;
+use Update\MigrationManager;
+use Update\Migrations\Migration6;
 
-function begin_transaction(PDO $db): void{
-  if (!$db->inTransaction()){
-    $db->beginTransaction();
+function get_migration(int $id): ?AbstractMigrationProcess{
+  switch($id){
+    case 6:
+      return new Migration6();
+      
+    default:
+      return null;
   }
 }
 
-function upgrade_config(PDO $db, int $version): void{
-  if (!file_put_contents(CONFIG_FILE, SystemConfig::fromCurrentInstallation()->generate($version), LOCK_EX)){
-    die('Lightning Tracker tried updating to a new version and failed updating the configuration file.');
-  }
-  
-  if (isset($db) && $db->inTransaction()){
-    $db->commit();
-  }
-}
+$manager = new MigrationManager(MIGRATION_VERSION, MIGRATION_TASK);
 
-/**
- * @param string $path
- * @return string
- * @throws Exception
- */
-function read_sql_file(string $path): string{
-  $file = __DIR__.'/~database/'.$path;
-  $contents = file_get_contents($file);
-  
-  if ($contents === false){
-    throw new Exception('Error reading file \''.$path.'\'.');
-  }
-  
-  return $contents;
+try{
+  $db = DB::get();
+}catch(Exception $e){
+  die('Lightning Tracker tried updating to a new version but could not connect to the database.');
 }
 
 try{
-  if (!copy(CONFIG_FILE, CONFIG_BACKUP_FILE)){
-    die('Lightning Tracker tried updating to a new version and failed creating a backup configuration file.');
-  }
-  
-  $migration_version = INSTALLED_MIGRATION_VERSION;
-  
-  if ($migration_version === 1){
-    $db = DB::get();
+  while(($version = $manager->getCurrentVersion()) < TRACKER_MIGRATION_VERSION){
+    $migration = get_migration($version);
     
-    $db->exec('ALTER TABLE system_roles ADD special BOOL DEFAULT FALSE NOT NULL');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE tracker_roles ADD special BOOL DEFAULT FALSE NOT NULL');
-    
-    $stmt = $db->prepare(<<<SQL
-SELECT DISTINCT TABLE_NAME AS tbl, CONSTRAINT_NAME AS constr
-FROM information_schema.KEY_COLUMN_USAGE
-WHERE TABLE_SCHEMA = :db_name AND REFERENCED_TABLE_SCHEMA = TABLE_SCHEMA
-  AND (
-    (TABLE_NAME = 'issues' AND COLUMN_NAME = 'milestone_id') OR
-    (TABLE_NAME = 'tracker_user_settings' AND COLUMN_NAME = 'active_milestone') OR
-    (TABLE_NAME = 'tracker_user_settings' AND COLUMN_NAME = 'tracker_id')
-  )
-SQL
-    );
-    
-    $stmt->bindValue('db_name', DB_NAME);
-    $stmt->execute();
-    $rows = $stmt->fetchAll();
-    
-    foreach($rows as $row){
-      /** @noinspection SqlResolve */
-      $db->exec('ALTER TABLE `'.$row['tbl'].'` DROP FOREIGN KEY `'.$row['constr'].'`');
+    if ($migration === null){
+      die('Cannot automatically update the installed version.');
     }
     
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE milestones CHANGE id milestone_id INT NOT NULL AFTER tracker_id');
-    $db->exec('ALTER TABLE milestones DROP PRIMARY KEY');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE milestones ADD PRIMARY KEY (tracker_id, milestone_id)');
+    $tasks = $migration->getTasks();
     
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-ALTER TABLE issues
-  ADD FOREIGN KEY (`milestone_id`, `tracker_id`)
-    REFERENCES `milestones` (`milestone_id`, `tracker_id`)
-    ON UPDATE CASCADE
-    ON DELETE RESTRICT
-SQL
-    );
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-ALTER TABLE tracker_user_settings
-  ADD FOREIGN KEY (`active_milestone`, `tracker_id`)
-    REFERENCES `milestones` (`milestone_id`, `tracker_id`)
-    ON UPDATE CASCADE
-    ON DELETE CASCADE
-SQL
-    );
-    
-    begin_transaction($db);
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-INSERT INTO tracker_roles (tracker_id, title, special)
-SELECT tracker_id, 'Owner' AS title, TRUE AS special
-FROM tracker_roles
-GROUP BY tracker_id
-SQL
-    );
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-INSERT INTO tracker_members (tracker_id, user_id, role_id)
-SELECT t.id AS tracker_id, t.owner_id AS user_id, tr.id AS role_id
-FROM trackers t
-JOIN tracker_roles tr ON t.id = tr.tracker_id AND tr.title = 'Owner' AND tr.special = TRUE
-SQL
-    );
-    
-    /** @noinspection SqlWithoutWhere */
-    $db->exec('UPDATE milestones SET milestone_id = ordering');
-    
-    upgrade_config($db, $migration_version = 2);
-  }
-  
-  if ($migration_version === 2){
-    $db = DB::get();
-    
-    $db->exec('ALTER TABLE milestones MODIFY ordering MEDIUMINT NOT NULL');
-    
-    $stmt = $db->prepare(<<<SQL
-SELECT DISTINCT TABLE_NAME AS tbl, CONSTRAINT_NAME AS constr
-FROM information_schema.KEY_COLUMN_USAGE
-WHERE TABLE_SCHEMA = :db_name
-  AND REFERENCED_TABLE_SCHEMA = TABLE_SCHEMA
-  AND (TABLE_NAME = 'tracker_members' AND REFERENCED_TABLE_NAME = 'tracker_roles')
-SQL
-    );
-    
-    $stmt->bindValue('db_name', DB_NAME);
-    $stmt->execute();
-    $rows = $stmt->fetchAll();
-    
-    foreach($rows as $row){
-      /** @noinspection SqlResolve */
-      $db->exec('ALTER TABLE `'.$row['tbl'].'` DROP FOREIGN KEY `'.$row['constr'].'`');
+    while(($id = $manager->getCurrentTask()) < count($tasks)){
+      $task = $tasks[$id];
+      $task->prepare($db);
+      $task->execute($db);
+      $task->finalize($db);
+      $manager->finishTask();
     }
     
-    /** @noinspection SqlResolve */
-    $db->exec('DROP TABLE tracker_role_perms');
-    /** @noinspection SqlResolve */
-    $db->exec('DROP TABLE tracker_roles');
-    
-    $db->exec(read_sql_file('TrackerRoleTable.sql'));
-    $db->exec(read_sql_file('TrackerRolePermTable.sql'));
-    
-    /** @noinspection SqlResolve */
-    $db->exec('UPDATE tracker_members SET role_id = NULL');
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-ALTER TABLE tracker_members
-  ADD FOREIGN KEY (`role_id`, `tracker_id`)
-    REFERENCES `tracker_roles` (`role_id`, `tracker_id`)
-    ON UPDATE CASCADE
-    ON DELETE RESTRICT
-SQL
-    );
-    
-    begin_transaction($db);
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-INSERT INTO tracker_roles (tracker_id, role_id, title, ordering, special)
-SELECT t.id, 1 AS role_id, 'Owner' AS title, 0 AS ordering, TRUE AS special
-FROM trackers t
-GROUP BY t.id
-SQL
-    );
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-INSERT INTO tracker_members (tracker_id, user_id, role_id)
-SELECT t.id AS tracker_id, t.owner_id AS user_id, tr.role_id AS role_id
-FROM trackers t
-JOIN tracker_roles tr ON t.id = tr.tracker_id AND tr.title = 'Owner' AND tr.special = TRUE
-ON DUPLICATE KEY UPDATE role_id = tr.role_id
-SQL
-    );
-    
-    upgrade_config($db, $migration_version = 3);
-  }
-  
-  if ($migration_version === 3){
-    $db = DB::get();
-    
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE tracker_role_perms MODIFY permission ENUM (\'settings\', \'members.list\', \'members.manage\', \'milestones.manage\', \'issues.create\', \'issues.fields.all\', \'issues.edit.all\', \'issues.delete.all\') NOT NULL');
-    
-    begin_transaction($db);
-    
-    /** @noinspection SqlResolve */
-    $db->exec(<<<SQL
-INSERT IGNORE INTO tracker_role_perms (tracker_id, role_id, permission)
-SELECT tr.tracker_id AS tracker_id, tr.role_id AS role_id, 'issues.fields.all' AS permission
-FROM tracker_roles tr
-WHERE tr.title = 'Developer'
-SQL
-    );
-    
-    upgrade_config($db, $migration_version = 4);
-  }
-  
-  if ($migration_version === 4){
-    $db = DB::get();
-    
-    $db->exec('RENAME TABLE trackers TO projects');
-    $db->exec('RENAME TABLE tracker_roles TO project_roles');
-    $db->exec('RENAME TABLE tracker_role_perms TO project_role_perms');
-    $db->exec('RENAME TABLE tracker_members TO project_members');
-    $db->exec('RENAME TABLE tracker_user_settings TO project_user_settings');
-    
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_roles CHANGE tracker_id project_id INT NOT NULL');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_role_perms CHANGE tracker_id project_id INT NOT NULL');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_members CHANGE tracker_id project_id INT NOT NULL');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_user_settings CHANGE tracker_id project_id INT NOT NULL');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE issues CHANGE tracker_id project_id INT NOT NULL');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE milestones CHANGE tracker_id project_id INT NOT NULL');
-    
-    upgrade_config($db, $migration_version = 5);
-  }
-  
-  if ($migration_version === 5){
-    $db = DB::get();
-    
-    $db->exec('RENAME TABLE project_role_perms TO project_role_permissions');
-    $db->exec('RENAME TABLE system_role_perms TO system_role_permissions');
-    $db->exec('ALTER TABLE system_role_permissions MODIFY permission ENUM (\'settings\', \'projects.list\', \'projects.list.all\', \'projects.create\', \'projects.manage\', \'users.list\', \'users.view.emails\', \'users.create\', \'users.manage\') NOT NULL');
-    
-    upgrade_config($db, $migration_version = 6);
-  }
-  
-  if ($migration_version === 6){
-    $db = DB::get();
-    
-    $stmt = $db->prepare(<<<SQL
-SELECT DISTINCT TABLE_NAME AS tbl, CONSTRAINT_NAME AS constr
-FROM information_schema.KEY_COLUMN_USAGE
-WHERE TABLE_SCHEMA = :db_name AND REFERENCED_TABLE_SCHEMA = TABLE_SCHEMA
-SQL
-    );
-    
-    $stmt->bindValue('db_name', DB_NAME);
-    $stmt->execute();
-    $rows = $stmt->fetchAll();
-    
-    foreach($rows as $row){
-      /** @noinspection SqlResolve */
-      $db->exec('ALTER TABLE `'.$row['tbl'].'` DROP FOREIGN KEY `'.$row['constr'].'`');
-    }
-    
-    $db->exec('ALTER TABLE users ADD public_id CHAR(9) NOT NULL FIRST');
-    
-    $db->exec('ALTER TABLE issues CHANGE author_id author_id_old INT NULL');
-    $db->exec('ALTER TABLE issues CHANGE assignee_id assignee_id_old INT NULL');
-    $db->exec('ALTER TABLE project_members CHANGE user_id user_id_old INT NOT NULL');
-    $db->exec('ALTER TABLE projects CHANGE owner_id owner_id_old INT NOT NULL');
-    $db->exec('ALTER TABLE project_user_settings CHANGE user_id user_id_old INT NOT NULL');
-    $db->exec('ALTER TABLE user_logins CHANGE id id_old INT NOT NULL');
-    
-    $db->exec('ALTER TABLE project_members DROP PRIMARY KEY');
-    $db->exec('ALTER TABLE project_user_settings DROP PRIMARY KEY');
-    $db->exec('ALTER TABLE user_logins DROP PRIMARY KEY');
-  
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE issues ADD author_id CHAR(9) NULL AFTER author_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE issues ADD assignee_id CHAR(9) NULL AFTER assignee_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_members ADD user_id CHAR(9) NOT NULL AFTER user_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE projects ADD owner_id CHAR(9) NOT NULL AFTER owner_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_user_settings ADD user_id CHAR(9) NOT NULL AFTER user_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE user_logins ADD id CHAR(9) NOT NULL AFTER id_old');
-    
-    $stmt = $db->query('SELECT id FROM users');
-    
-    while(($res = $stmt->fetchColumn()) !== false){
-      /** @noinspection SqlResolve */
-      $s2 = $db->prepare('UPDATE users SET public_id = ? WHERE id = ?');
-      $s2->bindValue(1, UserId::generateNew());
-      $s2->bindValue(2, (int)$res, PDO::PARAM_INT);
-      $s2->execute();
-    }
-  
-    /** @noinspection SqlResolve, SqlWithoutWhere */
-    $db->exec('UPDATE issues SET author_id = (SELECT u.public_id FROM users u WHERE u.id = author_id_old)');
-    /** @noinspection SqlResolve, SqlWithoutWhere */
-    $db->exec('UPDATE issues SET assignee_id = (SELECT u.public_id FROM users u WHERE u.id = assignee_id_old)');
-    /** @noinspection SqlResolve, SqlWithoutWhere */
-    $db->exec('UPDATE project_members SET user_id = (SELECT u.public_id FROM users u WHERE u.id = user_id_old)');
-    /** @noinspection SqlResolve, SqlWithoutWhere */
-    $db->exec('UPDATE projects SET owner_id = (SELECT u.public_id FROM users u WHERE u.id = owner_id_old)');
-    /** @noinspection SqlResolve, SqlWithoutWhere */
-    $db->exec('UPDATE project_user_settings SET user_id = (SELECT u.public_id FROM users u WHERE u.id = user_id_old)');
-    /** @noinspection SqlResolve, SqlWithoutWhere */
-    $db->exec('UPDATE user_logins SET id = (SELECT u.public_id FROM users u WHERE u.id = id_old)');
-    
-    $db->exec('ALTER TABLE users DROP id');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE users CHANGE public_id id CHAR(9) NOT NULL');
-    $db->exec('ALTER TABLE users ADD PRIMARY KEY (id)');
-  
-    $db->exec('ALTER TABLE project_members ADD PRIMARY KEY (project_id, user_id)');
-    $db->exec('ALTER TABLE project_user_settings ADD PRIMARY KEY (project_id, user_id)');
-    $db->exec('ALTER TABLE user_logins ADD PRIMARY KEY (id, token)');
-    
-    $db->exec('ALTER TABLE issues ADD CONSTRAINT fk__issue__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE issues ADD CONSTRAINT fk__issue__author FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE SET NULL');
-    $db->exec('ALTER TABLE issues ADD CONSTRAINT fk__issue__assignee FOREIGN KEY (`assignee_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE SET NULL');
-    $db->exec('ALTER TABLE issues ADD CONSTRAINT fk__issue__milestone FOREIGN KEY (`milestone_id`, `project_id`) REFERENCES `milestones` (`milestone_id`, `project_id`) ON UPDATE CASCADE ON DELETE RESTRICT');
-    $db->exec('ALTER TABLE issues ADD CONSTRAINT fk__issue__scale FOREIGN KEY (`scale`) REFERENCES `issue_weights` (`scale`) ON UPDATE RESTRICT ON DELETE RESTRICT');
-    $db->exec('ALTER TABLE milestones ADD CONSTRAINT fk__milestone__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE project_members ADD CONSTRAINT fk__project_member__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE project_members ADD CONSTRAINT fk__project_member__user FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE project_members ADD CONSTRAINT fk__project_member__role FOREIGN KEY (`role_id`, `project_id`) REFERENCES `project_roles` (`role_id`, `project_id`) ON UPDATE CASCADE ON DELETE RESTRICT');
-    $db->exec('ALTER TABLE project_role_permissions ADD CONSTRAINT fk__project_role_permission__role FOREIGN KEY (`role_id`, `project_id`) REFERENCES `project_roles` (`role_id`, `project_id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE project_roles ADD CONSTRAINT fk__project_role__project FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON UPDATE CASCADE ON DELETE CASCADE ');
-    $db->exec('ALTER TABLE projects ADD CONSTRAINT fk__project__owner FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE RESTRICT');
-    $db->exec('ALTER TABLE project_user_settings ADD CONSTRAINT fk__project_user_setting__user FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE project_user_settings ADD CONSTRAINT fk__project_user_setting__active_milestone FOREIGN KEY (`active_milestone`, `project_id`) REFERENCES `milestones` (`milestone_id`, `project_id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE system_role_permissions ADD CONSTRAINT fk__system_role_permission__role FOREIGN KEY (`role_id`) REFERENCES `system_roles` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE user_logins ADD CONSTRAINT fk__user_login__user FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE');
-    $db->exec('ALTER TABLE users ADD CONSTRAINT fk__user__role FOREIGN KEY (`role_id`) REFERENCES `system_roles` (`id`) ON UPDATE CASCADE ON DELETE SET NULL');
-  
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE issues DROP author_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE issues DROP assignee_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_members DROP user_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE projects DROP owner_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE project_user_settings DROP user_id_old');
-    /** @noinspection SqlResolve */
-    $db->exec('ALTER TABLE user_logins DROP id_old');
-    
-    upgrade_config($db, $migration_version = 7);
+    $manager->finishVersion();
   }
 }catch(Exception $e){
-  if (isset($db) && $db->inTransaction()){
+  Log::critical($e);
+  
+  if ($db->inTransaction()){
     $db->rollBack();
   }
   
-  Log::critical($e);
-  die('Lightning Tracker tried updating to a new version and encountered an unexpected error. Please check the server logs.');
+  die('Lightning Tracker tried updating to a new version and encountered an unexpected error (migration '.$manager->getCurrentVersion().', task '.$manager->getCurrentTask().'). Please check the server logs.');
 }
 ?>