summaryrefslogtreecommitdiffstats
path: root/system/libraries/Migration.php
diff options
context:
space:
mode:
Diffstat (limited to 'system/libraries/Migration.php')
-rw-r--r--system/libraries/Migration.php400
1 files changed, 274 insertions, 126 deletions
diff --git a/system/libraries/Migration.php b/system/libraries/Migration.php
index 241ce1e59..2a87d9d7c 100644
--- a/system/libraries/Migration.php
+++ b/system/libraries/Migration.php
@@ -1,19 +1,41 @@
-<?php defined('BASEPATH') OR exit('No direct script access allowed');
+<?php
/**
* CodeIgniter
*
- * An open source application development framework for PHP 5.1.6 or newer
+ * An open source application development framework for PHP
*
- * @package CodeIgniter
- * @author EllisLab Dev Team
- * @copyright Copyright (c) 2006 - 2014, EllisLab, Inc.
- * @license http://codeigniter.com/user_guide/license.html
- * @link http://codeigniter.com
- * @since Version 1.0
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @package CodeIgniter
+ * @author EllisLab Dev Team
+ * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
+ * @copyright Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
+ * @license http://opensource.org/licenses/MIT MIT License
+ * @link https://codeigniter.com
+ * @since Version 3.0.0
* @filesource
*/
-
-// ------------------------------------------------------------------------
+defined('BASEPATH') OR exit('No direct script access allowed');
/**
* Migration Class
@@ -29,26 +51,82 @@
*/
class CI_Migration {
+ /**
+ * Whether the library is enabled
+ *
+ * @var bool
+ */
protected $_migration_enabled = FALSE;
+
+ /**
+ * Migration numbering type
+ *
+ * @var bool
+ */
+ protected $_migration_type = 'sequential';
+
+ /**
+ * Path to migration classes
+ *
+ * @var string
+ */
protected $_migration_path = NULL;
+
+ /**
+ * Current migration version
+ *
+ * @var mixed
+ */
protected $_migration_version = 0;
+ /**
+ * Database table with migration info
+ *
+ * @var string
+ */
+ protected $_migration_table = 'migrations';
+
+ /**
+ * Whether to automatically run migrations
+ *
+ * @var bool
+ */
+ protected $_migration_auto_latest = FALSE;
+
+ /**
+ * Migration basename regex
+ *
+ * @var string
+ */
+ protected $_migration_regex;
+
+ /**
+ * Error message
+ *
+ * @var string
+ */
protected $_error_string = '';
+ /**
+ * Initialize Migration Class
+ *
+ * @param array $config
+ * @return void
+ */
public function __construct($config = array())
{
- # Only run this constructor on main library load
- if (get_parent_class($this) !== FALSE)
+ // Only run this constructor on main library load
+ if ( ! in_array(get_class($this), array('CI_Migration', config_item('subclass_prefix').'Migration'), TRUE))
{
return;
}
foreach ($config as $key => $val)
{
- $this->{'_' . $key} = $val;
+ $this->{'_'.$key} = $val;
}
- log_message('debug', 'Migrations class initialized');
+ log_message('info', 'Migrations Class Initialized');
// Are they trying to use migrations while it is disabled?
if ($this->_migration_enabled !== TRUE)
@@ -57,7 +135,7 @@ class CI_Migration {
}
// If not set, set it
- $this->_migration_path == '' AND $this->_migration_path = APPPATH . 'migrations/';
+ $this->_migration_path !== '' OR $this->_migration_path = APPPATH.'migrations/';
// Add trailing slash if not set
$this->_migration_path = rtrim($this->_migration_path, '/').'/';
@@ -68,16 +146,39 @@ class CI_Migration {
// They'll probably be using dbforge
$this->load->dbforge();
+ // Make sure the migration table name was set.
+ if (empty($this->_migration_table))
+ {
+ show_error('Migrations configuration file (migration.php) must have "migration_table" set.');
+ }
+
+ // Migration basename regex
+ $this->_migration_regex = ($this->_migration_type === 'timestamp')
+ ? '/^\d{14}_(\w+)$/'
+ : '/^\d{3}_(\w+)$/';
+
+ // Make sure a valid migration numbering type was set.
+ if ( ! in_array($this->_migration_type, array('sequential', 'timestamp')))
+ {
+ show_error('An invalid migration numbering type was specified: '.$this->_migration_type);
+ }
+
// If the migrations table is missing, make it
- if ( ! $this->db->table_exists('migrations'))
+ if ( ! $this->db->table_exists($this->_migration_table))
{
$this->dbforge->add_field(array(
- 'version' => array('type' => 'INT', 'constraint' => 3),
+ 'version' => array('type' => 'BIGINT', 'constraint' => 20),
));
- $this->dbforge->create_table('migrations', TRUE);
+ $this->dbforge->create_table($this->_migration_table, TRUE);
+
+ $this->db->insert($this->_migration_table, array('version' => 0));
+ }
- $this->db->insert('migrations', array('version' => 0));
+ // Do we auto migrate to the latest migration?
+ if ($this->_migration_auto_latest === TRUE && ! $this->latest())
+ {
+ show_error($this->error_string());
}
}
@@ -89,154 +190,166 @@ class CI_Migration {
* Calls each migration step required to get to the schema version of
* choice
*
- * @param int Target schema version
- * @return mixed TRUE if already latest, FALSE if failed, int if upgraded
+ * @param string $target_version Target schema version
+ * @return mixed TRUE if no migrations are found, current version string on success, FALSE on failure
*/
public function version($target_version)
{
- $start = $current_version = $this->_get_version();
- $stop = $target_version;
+ // Note: We use strings, so that timestamp versions work on 32-bit systems
+ $current_version = $this->_get_version();
- if ($target_version > $current_version)
+ if ($this->_migration_type === 'sequential')
{
- // Moving Up
- ++$start;
- ++$stop;
- $step = 1;
+ $target_version = sprintf('%03d', $target_version);
}
else
{
- // Moving Down
- $step = -1;
+ $target_version = (string) $target_version;
}
- $method = ($step === 1) ? 'up' : 'down';
- $migrations = array();
+ $migrations = $this->find_migrations();
- // We now prepare to actually DO the migrations
- // But first let's make sure that everything is the way it should be
- for ($i = $start; $i != $stop; $i += $step)
+ if ($target_version > 0 && ! isset($migrations[$target_version]))
{
- $f = glob(sprintf($this->_migration_path . '%03d_*.php', $i));
+ $this->_error_string = sprintf($this->lang->line('migration_not_found'), $target_version);
+ return FALSE;
+ }
- // Only one migration per step is permitted
- if (count($f) > 1)
- {
- $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $i);
- return FALSE;
- }
+ if ($target_version > $current_version)
+ {
+ $method = 'up';
+ }
+ elseif ($target_version < $current_version)
+ {
+ $method = 'down';
+ // We need this so that migrations are applied in reverse order
+ krsort($migrations);
+ }
+ else
+ {
+ // Well, there's nothing to migrate then ...
+ return TRUE;
+ }
- // Migration step not found
- if (count($f) == 0)
+ // 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)
+ {
+ // 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')
{
- // If trying to migrate up to a version greater than the last
- // existing one, migrate to the last one.
- if ($step == 1)
+ if ($number <= $current_version)
+ {
+ continue;
+ }
+ elseif ($number > $target_version)
{
break;
}
-
- // If trying to migrate down but we're missing a step,
- // something must definitely be wrong.
- $this->_error_string = sprintf($this->lang->line('migration_not_found'), $i);
- return FALSE;
}
-
- $file = basename($f[0]);
- $name = basename($f[0], '.php');
-
- // Filename validations
- if (preg_match('/^\d{3}_(\w+)$/', $name, $match))
+ else
{
- $match[1] = strtolower($match[1]);
-
- // Cannot repeat a migration at different steps
- if (in_array($match[1], $migrations))
+ if ($number > $current_version)
{
- $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $match[1]);
- return FALSE;
+ continue;
}
-
- include $f[0];
- $class = 'Migration_' . ucfirst($match[1]);
-
- if ( ! class_exists($class))
+ elseif ($number <= $target_version)
{
- $this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class);
- return FALSE;
+ break;
}
+ }
- if ( ! is_callable(array($class, $method)))
+ // Check for sequence gaps
+ if ($this->_migration_type === 'sequential')
+ {
+ if (isset($previous) && abs($number - $previous) > 1)
{
- $this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
+ $this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number);
return FALSE;
}
- $migrations[] = $match[1];
+ $previous = $number;
}
- else
+
+ include_once($file);
+ $class = 'Migration_'.ucfirst(strtolower($this->_get_migration_name(basename($file, '.php'))));
+
+ // Validate the migration file structure
+ if ( ! class_exists($class, FALSE))
{
- $this->_error_string = sprintf($this->lang->line('migration_invalid_filename'), $file);
+ $this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class);
+ return FALSE;
+ }
+ elseif ( ! is_callable(array($class, $method)))
+ {
+ $this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
return FALSE;
}
- }
-
- log_message('debug', 'Current migration: ' . $current_version);
- $version = $i + ($step == 1 ? -1 : 0);
+ $pending[$number] = array($class, $method);
+ }
- // If there is nothing to do so quit
- if ($migrations === array())
+ // Now just run the necessary migrations
+ foreach ($pending as $number => $migration)
{
- return TRUE;
- }
+ log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number);
- log_message('debug', 'Migrating from ' . $method . ' to version ' . $version);
+ $migration[0] = new $migration[0];
+ call_user_func($migration);
+ $current_version = $number;
+ $this->_update_version($current_version);
+ }
- // Loop through the migrations
- foreach ($migrations AS $migration)
+ // This is necessary when moving down, since the the last migration applied
+ // will be the down() method for the next migration up from the target
+ if ($current_version <> $target_version)
{
- // Run the migration class
- $class = 'Migration_' . ucfirst(strtolower($migration));
- call_user_func(array(new $class, $method));
-
- $current_version += $step;
+ $current_version = $target_version;
$this->_update_version($current_version);
}
log_message('debug', 'Finished migrating to '.$current_version);
-
return $current_version;
}
// --------------------------------------------------------------------
/**
- * Set's the schema to the latest migration
+ * Sets the schema to the latest migration
*
- * @return mixed true if already latest, false if failed, int if upgraded
+ * @return mixed Current version string on success, FALSE on failure
*/
public function latest()
{
- if ( ! $migrations = $this->find_migrations())
+ $migrations = $this->find_migrations();
+
+ if (empty($migrations))
{
$this->_error_string = $this->lang->line('migration_none_found');
- return false;
+ return FALSE;
}
$last_migration = basename(end($migrations));
// Calculate the last migration step from existing migration
- // filenames and procceed to the standard version migration
- return $this->version((int) substr($last_migration, 0, 3));
+ // filenames and proceed to the standard version migration
+ return $this->version($this->_get_migration_number($last_migration));
}
// --------------------------------------------------------------------
/**
- * Set's the schema to the migration version set in config
+ * Sets the schema to the migration version set in config
*
- * @return mixed true if already current, false if failed, int if upgraded
+ * @return mixed TRUE if no migrations are found, current version string on success, FALSE on failure
*/
public function current()
{
@@ -258,28 +371,66 @@ class CI_Migration {
// --------------------------------------------------------------------
/**
- * Set's the schema to the latest migration
+ * Retrieves list of available migration scripts
*
- * @return mixed true if already latest, false if failed, int if upgraded
+ * @return array list of migration file paths sorted by version
*/
- protected function find_migrations()
+ public function find_migrations()
{
- // Load all *_*.php files in the migrations path
- $files = glob($this->_migration_path . '*_*.php');
- $file_count = count($files);
+ $migrations = array();
- for ($i = 0; $i < $file_count; $i++)
+ // Load all *_*.php files in the migrations path
+ foreach (glob($this->_migration_path.'*_*.php') as $file)
{
- // Mark wrongly formatted files as false for later filtering
- $name = basename($files[$i], '.php');
- if ( ! preg_match('/^\d{3}_(\w+)$/', $name))
+ $name = basename($file, '.php');
+
+ // Filter out non-migration files
+ if (preg_match($this->_migration_regex, $name))
{
- $files[$i] = FALSE;
+ $number = $this->_get_migration_number($name);
+
+ // There cannot be duplicate migration numbers
+ if (isset($migrations[$number]))
+ {
+ $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $number);
+ show_error($this->_error_string);
+ }
+
+ $migrations[$number] = $file;
}
}
- sort($files);
- return $files;
+ ksort($migrations);
+ return $migrations;
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Extracts the migration number from a filename
+ *
+ * @param string $migration
+ * @return string Numeric portion of a migration filename
+ */
+ protected function _get_migration_number($migration)
+ {
+ return sscanf($migration, '%[0-9]+', $number)
+ ? $number : '0';
+ }
+
+ // --------------------------------------------------------------------
+
+ /**
+ * Extracts the migration class name from a filename
+ *
+ * @param string $migration
+ * @return string text portion of a migration filename
+ */
+ protected function _get_migration_name($migration)
+ {
+ $parts = explode('_', $migration);
+ array_shift($parts);
+ return implode('_', $parts);
}
// --------------------------------------------------------------------
@@ -287,12 +438,12 @@ class CI_Migration {
/**
* Retrieves current schema version
*
- * @return int Current Migration
+ * @return string Current migration version
*/
protected function _get_version()
{
- $row = $this->db->get('migrations')->row();
- return $row ? $row->version : 0;
+ $row = $this->db->select('version')->get($this->_migration_table)->row();
+ return $row ? $row->version : '0';
}
// --------------------------------------------------------------------
@@ -300,13 +451,13 @@ class CI_Migration {
/**
* Stores the current schema version
*
- * @param int Migration reached
- * @return bool
+ * @param string $migration Migration reached
+ * @return void
*/
- protected function _update_version($migrations)
+ protected function _update_version($migration)
{
- return $this->db->update('migrations', array(
- 'version' => $migrations
+ $this->db->update($this->_migration_table, array(
+ 'version' => $migration
));
}
@@ -315,7 +466,7 @@ class CI_Migration {
/**
* Enable the use of CI super-global
*
- * @param mixed $var
+ * @param string $var
* @return mixed
*/
public function __get($var)
@@ -324,6 +475,3 @@ class CI_Migration {
}
}
-
-/* End of file Migration.php */
-/* Location: ./system/libraries/Migration.php */ \ No newline at end of file