diff options
-rw-r--r-- | application/config/migration.php | 18 | ||||
-rw-r--r-- | system/language/english/migration_lang.php | 1 | ||||
-rw-r--r-- | system/libraries/Migration.php | 230 | ||||
-rw-r--r-- | user_guide_src/source/changelog.rst | 3 | ||||
-rw-r--r-- | user_guide_src/source/libraries/migration.rst | 84 |
5 files changed, 207 insertions, 129 deletions
diff --git a/application/config/migration.php b/application/config/migration.php index 7645ade7c..476da1b8e 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -39,6 +39,24 @@ $config['migration_enabled'] = FALSE; /* |-------------------------------------------------------------------------- +| Migration Style +|-------------------------------------------------------------------------- +| +| Migration file names may be based on a sequential identifier or on +| a timestamp. Options are: +| +| 'sequential' = Default migration naming (001_add_blog.php) +| 'timestamp' = Timestamp migration naming (20121031104401_add_blog.php) +| Use timestamp format YYYYMMDDHHIISS. +| +| If this configuration value is missing the Migration library defaults +| to 'sequential' for backward compatibility. +| +*/ +$config['migration_style'] = 'timestamp'; + +/* +|-------------------------------------------------------------------------- | Migrations table |-------------------------------------------------------------------------- | diff --git a/system/language/english/migration_lang.php b/system/language/english/migration_lang.php index 5753c00bf..a262d3018 100644 --- a/system/language/english/migration_lang.php +++ b/system/language/english/migration_lang.php @@ -27,6 +27,7 @@ $lang['migration_none_found'] = 'No migrations were found.'; $lang['migration_not_found'] = 'No migration could be found with the version number: %d.'; +$lang['migration_sequence_gap'] = 'There is a gap in the migration sequence near version number: %d.'; $lang['migration_multiple_version'] = 'There are multiple migrations with the same version number: %d.'; $lang['migration_class_doesnt_exist'] = 'The migration class "%s" could not be found.'; $lang['migration_missing_up_method'] = 'The migration class "%s" is missing an "up" method.'; diff --git a/system/libraries/Migration.php b/system/libraries/Migration.php index 5d637d44a..2a06aa011 100644 --- a/system/libraries/Migration.php +++ b/system/libraries/Migration.php @@ -45,6 +45,13 @@ class CI_Migration { * @var bool */ protected $_migration_enabled = FALSE; + + /** + * Migration numbering style + * + * @var bool + */ + protected $_migration_style = 'sequential'; /** * Path to migration classes @@ -73,6 +80,13 @@ class CI_Migration { * @var bool */ protected $_migration_auto_latest = FALSE; + + /** + * Migration basename regex + * + * @var bool + */ + protected $_migration_regex = NULL; /** * Error message @@ -125,12 +139,21 @@ class CI_Migration { { show_error('Migrations configuration file (migration.php) must have "migration_table" set.'); } + + // Migration basename regex + $this->_migration_regex = $this->_migration_style === 'timestamp' ? '/^\d{14}_(\w+)$/' : '/^\d{3}_(\w+)$/'; + + // Make sure a valid migration numbering style was set. + if ( ! in_array($this->_migration_style, array('sequential', 'timestamp'))) + { + show_error('An invalid migration numbering style was specified: '.$this->_migration_style); + } // If the migrations table is missing, make it if ( ! $this->db->table_exists($this->_migration_table)) { $this->dbforge->add_field(array( - 'version' => array('type' => 'INT', 'constraint' => 3), + 'version' => array('type' => 'BIGINT', 'constraint' => 3), )); $this->dbforge->create_table($this->_migration_table, TRUE); @@ -158,113 +181,80 @@ class CI_Migration { */ public function version($target_version) { - $start = $current_version = $this->_get_version(); - $stop = $target_version; - + $current_version = (int) $this->_get_version(); + $target_version = (int) $target_version; + + $migrations = $this->find_migrations(); + + if ($target_version > 0 AND ! isset($migrations[$target_version])) + { + $this->_error_string = sprintf($this->lang->line('migration_not_found'), $target_version); + return FALSE; + } + if ($target_version > $current_version) { // Moving Up - ++$start; - ++$stop; - $step = 1; + $method = 'up'; } else { - // Moving Down - $step = -1; + // Moving Down, apply in reverse order + $method = 'down'; + krsort($migrations); } - $method = $step === 1 ? 'up' : 'down'; - $migrations = array(); - - // 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 (empty($migrations)) { - $f = glob(sprintf($this->_migration_path.'%03d_*.php', $i)); + return TRUE; + } + + $previous = FALSE; - // Only one migration per step is permitted - if (count($f) > 1) + // Validate all available migrations, and run the ones within our target range + foreach ($migrations as $number => $file) + { + // Check for sequence gaps + if ($this->_migration_style === 'sequential' AND $previous !== FALSE AND abs($number - $previous) > 1) { - $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $i); + $this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number); return FALSE; } + + include $file; + $class = 'Migration_'.ucfirst(strtolower($this->_get_migration_name(basename($file, '.php')))); - // Migration step not found - if (count($f) === 0) + // Validate the migration file structure + if ( ! class_exists($class)) { - // If trying to migrate up to a version greater than the last - // existing one, migrate to the last one. - if ($step === 1) - { - 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); + $this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class); return FALSE; } - - $file = basename($f[0]); - $name = basename($f[0], '.php'); - - // Filename validations - if (preg_match('/^\d{3}_(\w+)$/', $name, $match)) - { - $match[1] = strtolower($match[1]); - - // Cannot repeat a migration at different steps - if (in_array($match[1], $migrations)) - { - $this->_error_string = sprintf($this->lang->line('migration_multiple_version'), $match[1]); - return FALSE; - } - - include $f[0]; - $class = 'Migration_'.ucfirst($match[1]); - - if ( ! class_exists($class)) - { - $this->_error_string = sprintf($this->lang->line('migration_class_doesnt_exist'), $class); - return FALSE; - } - - if ( ! is_callable(array($class, $method))) - { - $this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class); - return FALSE; - } - - $migrations[] = $match[1]; - } - else + elseif ( ! is_callable(array($class, $method))) { - $this->_error_string = sprintf($this->lang->line('migration_invalid_filename'), $file); + $this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class); return FALSE; } + + $previous = $number; + + // Run migrations that are inside the target range + if ( + ($method === 'up' AND $number > $current_version AND $number <= $target_version) OR + ($method === 'down' AND $number <= $current_version AND $number > $target_version) + ) { + log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number); + call_user_func(array(new $class, $method)); + $current_version = $number; + $this->_update_version($current_version); + } } - - log_message('debug', 'Current migration: '.$current_version); - - $version = $i + ($step === 1 ? -1 : 0); - - // If there is nothing to do so quit - if ($migrations === array()) - { - return TRUE; - } - - log_message('debug', 'Migrating from '.$method.' to version '.$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); } @@ -282,17 +272,19 @@ class CI_Migration { */ 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; } $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) $last_migration); + return $this->version($this->_get_migration_number($last_migration)); } // -------------------------------------------------------------------- @@ -326,22 +318,60 @@ class CI_Migration { * * @return array list of migration file paths sorted by version */ - protected function find_migrations() + public function find_migrations() { + $migrations = array(); + // Load all *_*.php files in the migrations path - $files = glob($this->_migration_path.'*_*.php'); - - for ($i = 0, $c = count($files); $i < $c; $i++) + foreach (glob($this->_migration_path.'*_*.php') as $file) { - // Mark wrongly formatted files as false for later filtering - if ( ! preg_match('/^\d{3}_(\w+)$/', basename($files[$i], '.php'))) + $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 + * + * @return int Numeric portion of a migration filename + */ + protected function _get_migration_number($migration) + { + $parts = explode('_', $migration); + return (int) $parts[0]; + } + + // -------------------------------------------------------------------- + + /** + * Extracts the migration class name from a filename + * + * @return string text portion of a migration filename + */ + protected function _get_migration_name($migration) + { + $parts = explode('_', $migration); + array_shift($parts); + return implode('_', $parts); } // -------------------------------------------------------------------- @@ -365,10 +395,10 @@ class CI_Migration { * @param int Migration reached * @return void Outputs a report of the migration */ - protected function _update_version($migrations) + protected function _update_version($migration) { return $this->db->update($this->_migration_table, array( - 'version' => $migrations + 'version' => $migration )); } diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst index 065daf54b..f45cc5cd7 100644 --- a/user_guide_src/source/changelog.rst +++ b/user_guide_src/source/changelog.rst @@ -227,6 +227,9 @@ Release Date: Not Released - Added support for hashing algorithms other than SHA1 and MD5. - Removed previously deprecated ``sha1()`` method. - Changed :doc:`Language Library <libraries/language>` method ``load()`` to filter the language name with ``ctype_digit()``. + - :doc:`Migration Library <libraries/migration>` changes include: + - Added support for timestamp-based migrations (enabled by default) + - Added ``$config['migration_style']`` to allow switching between sequential migrations and timestamp migrations - Core diff --git a/user_guide_src/source/libraries/migration.rst b/user_guide_src/source/libraries/migration.rst index cb7d96a6d..246396171 100644 --- a/user_guide_src/source/libraries/migration.rst +++ b/user_guide_src/source/libraries/migration.rst @@ -13,17 +13,40 @@ run so all you have to do is update your application files and call **$this->migrate->current()** to work out which migrations should be run. The current version is found in **config/migration.php**. +******************** +Migration file names +******************** + +Each Migration is run in numeric order forward or backwards depending on the +method taken. Two numbering styles are available: + +* **Sequential:** each migration is numbered in sequence, starting with **001**. + Each number must be three digits, and there must not be any gaps in the + sequence. (This was the numbering scheme prior to CodeIgniter 3.0.) +* **Timestamp:** each migration is numbered using the timestamp when the migration + was created, in **YYYYMMDDHHIISS** format (e.g. **20121031100537**). This + helps prevent numbering conflicts when working in a team environment, and is + the preferred scheme in CodeIgniter 3.0 and later. + +The desired style may be selected using the **$config['migration_style']** +setting in your **migration.php** config file. + +Regardless of which numbering style you choose to use, prefix your migration +files with the migration number followed by an underscore and a descriptive +name for the migration. For example: + +* **001_add_blog.php** (sequential numbering) +* **20121031100537_add_blog.php** (timestamp numbering) + ****************** Create a Migration ****************** - -.. note:: Each Migration is run in numerical order forward or backwards - depending on the method taken. Use a prefix of 3 numbers followed by an - underscore for the filename of your migration. This will be the first migration for a new site which has a blog. All migrations go in the folder **application/migrations/** and have names such -as: **001_add_blog.php**.:: +as **20121031100537_add_blog.php**.:: + + <?php defined('BASEPATH') OR exit('No direct script access allowed'); @@ -47,7 +70,7 @@ as: **001_add_blog.php**.:: 'null' => TRUE, ), )); - + $this->dbforge->add_key('blog_id', TRUE); $this->dbforge->create_table('blog'); } @@ -55,6 +78,7 @@ as: **001_add_blog.php**.:: { $this->dbforge->drop_table('blog'); } + } Then in **application/config/migration.php** set **$config['migration_version'] = 1;**. @@ -65,25 +89,25 @@ Usage Example In this example some simple code is placed in **application/controllers/migrate.php** to update the schema.:: - $this->load->library('migration'); - - if ( ! $this->migration->current()) + <?php + + class Migrate extends CI_Controller { - show_error($this->migration->error_string()); + public function index() + { + $this->load->library('migration'); + + if ($this->migration->current() === FALSE) + { + show_error($this->migration->error_string()); + } + } } ****************** Function Reference ****************** -There are five available methods for the Migration class: - -- $this->migration->current(); -- $this->migration->error_string(); -- $this->migration->find_migrations(); -- $this->migration->latest(); -- $this->migration->version(); - $this->migration->current() ============================ @@ -124,14 +148,16 @@ Migration Preferences The following is a table of all the config options for migrations. -========================== ====================== ============= ============================================= -Preference Default Options Description -========================== ====================== ============= ============================================= -**migration_enabled** FALSE TRUE / FALSE Enable or disable migrations. -**migration_path** APPPATH.'migrations/' None The path to your migrations folder. -**migration_version** 0 None The current version your database should use. -**migration_table** migrations None The table name for storing the shema - version number. -**migration_auto_latest** FALSE TRUE / FALSE Enable or disable automatically - running migrations. -========================== ====================== ============= ============================================= +========================== ====================== ========================== ============================================= +Preference Default Options Description +========================== ====================== ========================== ============================================= +**migration_enabled** FALSE TRUE / FALSE Enable or disable migrations. +**migration_path** APPPATH.'migrations/' None The path to your migrations folder. +**migration_version** 0 None The current version your database should use. +**migration_table** migrations None The table name for storing the schema + version number. +**migration_auto_latest** FALSE TRUE / FALSE Enable or disable automatically + running migrations. +**migration_style** 'timestamp' 'timestamp' / 'sequential' The type of numeric identifier used to name + migration files. +========================== ====================== ========================== ============================================= |