Fix #4539
diff --git a/system/libraries/Migration.php b/system/libraries/Migration.php
index 7aefb6c..74bec3d 100644
--- a/system/libraries/Migration.php
+++ b/system/libraries/Migration.php
@@ -96,9 +96,9 @@
 	/**
 	 * Migration basename regex
 	 *
-	 * @var bool
+	 * @var string
 	 */
-	protected $_migration_regex = NULL;
+	protected $_migration_regex;
 
 	/**
 	 * Error message
@@ -217,31 +217,61 @@
 
 		if ($target_version > $current_version)
 		{
-			// Moving Up
 			$method = 'up';
 		}
 		else
 		{
-			// Moving Down, apply in reverse order
 			$method = 'down';
+			// We need this so that migrations are applied in reverse order
 			krsort($migrations);
 		}
 
-		if (empty($migrations))
-		{
-			return TRUE;
-		}
-
-		$previous = FALSE;
-
-		// Validate all available migrations, and run the ones within our target range
+		// Validate all available migrations within our target range.
+		//
+		// Unfortunately, we'll have to use another loop to run them
+		// in order to avoid leaving the procedure in a broken state.
+		//
+		// See https://github.com/bcit-ci/CodeIgniter/issues/4539
+		$pending = array();
 		foreach ($migrations as $number => $file)
 		{
-			// Check for sequence gaps
-			if ($this->_migration_type === 'sequential' && $previous !== FALSE && abs($number - $previous) > 1)
+			// Ignore versions out of our range.
+			//
+			// Because we've previously sorted the $migrations array depending on the direction,
+			// we can safely break the loop once we reach $target_version ...
+			if ($method === 'up')
 			{
-				$this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number);
-				return FALSE;
+				if ($number <= $current_version)
+				{
+					continue;
+				}
+				elseif ($number > $target_version)
+				{
+					break;
+				}
+			}
+			else
+			{
+				if ($number > $current_version)
+				{
+					continue;
+				}
+				elseif ($number <= $target_version)
+				{
+					break;
+				}
+			}
+
+			// Check for sequence gaps
+			if ($this->_migration_type === 'sequential')
+			{
+				if (isset($previous) && abs($number - $previous) > 1)
+				{
+					$this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number);
+					return FALSE;
+				}
+
+				$previous = $number;
 			}
 
 			include_once($file);
@@ -253,27 +283,27 @@
 				$this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class);
 				return FALSE;
 			}
-
-			$previous = $number;
-
-			// Run migrations that are inside the target range
-			if (
-				($method === 'up'   && $number > $current_version && $number <= $target_version) OR
-				($method === 'down' && $number <= $current_version && $number > $target_version)
-			)
+			// method_exists() returns true for non-public methods,
+			// while is_callable() can't be used without instantiating.
+			// Only get_class_methods() satisfies both conditions.
+			elseif ( ! in_array($method, array_map('strtolower', get_class_methods($class))))
 			{
-				$instance = new $class();
-				if ( ! is_callable(array($instance, $method)))
-				{
-					$this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
-					return FALSE;
-				}
-
-				log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number);
-				call_user_func(array($instance, $method));
-				$current_version = $number;
-				$this->_update_version($current_version);
+				$this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
+				return FALSE;
 			}
+
+			$pending[$number] = array($class, $method);
+		}
+
+		// Now just run the necessary migrations
+		foreach ($pending as $number => $migration)
+		{
+			log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number);
+
+			$migration[0] = new $migration[0];
+			call_user_func($migration);
+			$current_version = $number;
+			$this->_update_version($current_version);
 		}
 
 		// This is necessary when moving down, since the the last migration applied
@@ -285,7 +315,6 @@
 		}
 
 		log_message('debug', 'Finished migrating to '.$current_version);
-
 		return $current_version;
 	}