diff --git a/classes/minion/migration/manager.php b/classes/minion/migration/manager.php index e2a70ff..df7954d 100644 --- a/classes/minion/migration/manager.php +++ b/classes/minion/migration/manager.php @@ -118,91 +118,60 @@ class Minion_Migration_Manager { /** * Run migrations in the specified groups so as to reach specified targets * - * There are three methods for specifying target versions: * - * 1. Pass them in with the array of groups, i.e. - * - * array( - * group => target_version - * ) - * - * 2. Pass them in separately, with param1 containing an array of - * groups like: - * - * array( - * group, - * group2, - * ) - * - * And param2 containing an array structured in the same way as in #1 - * - * 3. Perform a mix of the above two methods - * - * It may seem odd to use two arrays to specify groups and versions, but - * it's this way to allow users to upgrade / downgrade all groups while - * migrating a specific group to a specific version - * - * If no groups are specified then migrations from all groups will be - * run and be brought up to the latest available version * * @param array Set of groups to update, empty array means all * @param array Versions for specified groups - * @param boolean The default direction (up/down) for migrations without a specific version * @return array Array of all migrations that were successfully applied */ - public function run_migration(array $groups = array(), $versions = array(), $default_direction = TRUE) + public function run_migration($group = array(), $target = TRUE) { - $migrations = $this->_model->fetch_required_migrations($groups, $versions, $default_direction); + list($migrations, $is_up) = $this->_model->fetch_required_migrations($group, $target); - foreach ($migrations as $path => $group) + $method = $is_up ? 'up' : 'down'; + + foreach ($migrations as $migration) { - $method = $group['direction'] ? 'up' : 'down'; + $filename = Minion_Migration_Util::get_filename_from_migration($migration); - foreach ($group['migrations'] as $migration) + if ( ! ($file = Kohana::find_file('migrations', $filename, FALSE))) { - $filename = Minion_Migration_Util::get_filename_from_migration($migration); - - if ( ! ($file = Kohana::find_file('migrations', $filename, FALSE))) - { - throw new Kohana_Exception( - 'Cannot load migration :migration (:file)', - array( - ':migration' => $migration['id'], - ':file' => $filename - ) - ); - } - - $class = Minion_Migration_Util::get_class_from_migration($migration); - - - include_once $file; - - $instance = new $class($migration); - - $db = $this->_get_db_instance($instance->get_database_connection()); - - try - { - $instance->$method($db); - } - catch(Database_Exception $e) - { - throw new Minion_Migration_Exception($e->getMessage(), $migration); - } - - - if ($this->_dry_run) - { - $this->_dry_run_sql[$path][$migration['timestamp']] = $db->reset_query_stack(); - } - else - { - $this->_model->mark_migration($migration, $group['direction']); - } - - $this->_executed_migrations[] = $migration; + throw new Kohana_Exception( + 'Cannot load migration :migration (:file)', + array( + ':migration' => $migration['id'], + ':file' => $filename + ) + ); } + + $class = Minion_Migration_Util::get_class_from_migration($migration); + + include_once $file; + + $instance = new $class($migration); + + $db = $this->_get_db_instance($instance->get_database_connection()); + + try + { + $instance->$method($db); + } + catch(Database_Exception $e) + { + throw new Minion_Migration_Exception($e->getMessage(), $migration); + } + + if ($this->_dry_run) + { + $this->_dry_run_sql[$path][$migration['timestamp']] = $db->reset_query_stack(); + } + else + { + $this->_model->mark_migration($migration, $is_up); + } + + $this->_executed_migrations[] = $migration; } } @@ -244,6 +213,9 @@ class Minion_Migration_Manager { { $this->_model->update_migration($installed[$migration], $available[$migration]); } + { + + } } diff --git a/classes/minion/task/db/migrate.php b/classes/minion/task/db/migrate.php index 73766cd..3c08c43 100644 --- a/classes/minion/task/db/migrate.php +++ b/classes/minion/task/db/migrate.php @@ -7,27 +7,21 @@ * * Available config options are: * - * --versions=[group:]version + * --migrate-down * - * Used to specify the version to migrate the database to. The group prefix - * is used to specify the target version of an individual group. Version - * specifies the target version, which can be either: + * Migrate the group(s) down * - * * A migration version (migrates up/down to that version) - * * TRUE (runs all migrations to get to the latest version) - * * FALSE (undoes all appled migrations) + * --migrate-up * - * An example of a migration version is 20101229015800 + * Migrate the group(s) up * - * If you specify TRUE / FALSE without a group then the default migration - * direction for groups without a specified version will be up / down respectively. + * --migrate-to=(timestamp|+up_migrations|down_migrations) * - * If you're only specifying a migration version then you *must* specify a group + * Migrate to a specific timestamp, or up $up_migrations, or down $down_migrations * * --groups=group[,group2[,group3...]] * - * A list of groups (under the migrations folder in the cascading - * filesystem) that will be used to source migration files. By default + * A list of groups that will be used to source migration files. By default * migrations will be loaded from all available groups * * --dry-run @@ -44,20 +38,16 @@ */ class Minion_Task_Db_Migrate extends Minion_Task { - - /* - * The default direction for migrations, TRUE = up, FALSE = down - * @var boolean - */ - protected $_default_direction = TRUE; - /** * A set of config options that this task accepts * @var array */ protected $_config = array( - 'versions', + 'group', 'groups', + 'migrate-up', + 'migrate-down', + 'migrate-to', 'dry-run', 'quiet' ); @@ -71,19 +61,36 @@ class Minion_Task_Db_Migrate extends Minion_Task { $k_config = Kohana::config('minion/migration'); - // Grab user input, using sensible defaults - $specified_groups = Arr::get($config, 'groups', NULL); - $versions = Arr::get($config, 'versions', NULL); - $dry_run = array_key_exists('dry-run', $config); - $quiet = array_key_exists('quiet', $config); + $groups = Arr::get($config, 'group', Arr::get($config, 'groups', NULL)); + $target = Arr::get($config, 'migrate-to', NULL); - $targets = $this->_parse_target_versions($versions); - $groups = $this->_parse_groups($specified_groups); + $dry_run = array_key_exists('dry-run', $config); + $quiet = array_key_exists('quiet', $config); + $up = array_key_exists('migrate-up', $config); + $down = array_key_exists('migrate-down', $config); + + $groups = $this->_parse_groups($groups); + + if ($target === NULL) + { + if ($up) + { + $target = TRUE; + } + elseif ($down) + { + $target = FALSE; + } + else + { + echo "No migration target specified\n"; + return; + } + } $db = Database::instance(); $model = new Model_Minion_Migration($db); - $model->ensure_table_exists(); $manager = new Minion_Migration_Manager($db, $model); @@ -91,13 +98,12 @@ class Minion_Task_Db_Migrate extends Minion_Task $manager // Sync the available migrations with those in the db ->sync_migration_files() - ->set_dry_run($dry_run); try { // Run migrations for specified groups & versions - $manager->run_migration($groups, $targets, $this->_default_direction); + $manager->run_migration($groups, $target); } catch(Minion_Migration_Exception $e) { @@ -106,7 +112,6 @@ class Minion_Task_Db_Migrate extends Minion_Task ->set('error', $e->getMessage()); } - $view = View::factory('minion/task/db/migrate') ->set('dry_run', $dry_run) ->set('quiet', $quiet) diff --git a/classes/model/minion/migration.php b/classes/model/minion/migration.php index eb82211..c945bbe 100644 --- a/classes/model/minion/migration.php +++ b/classes/model/minion/migration.php @@ -66,12 +66,12 @@ class Model_Minion_Migration extends Model public function get_group_statuses() { // Start out using all the installed groups - $groups = $this->fetch_current_versions('group', 'id'); + $groups = $this->fetch_current_versions('group'); $available = $this->available_migrations(); foreach ($available as $migration) { - if (array_key_exists($migration['id'], $groups)) + if (array_key_exists($migration['group'], $groups)) { continue; } @@ -283,12 +283,13 @@ class Model_Minion_Migration extends Model * @param string The groups to get migrations for * @param mixed Target version */ - public function fetch_required_migrations($group = NULL, $target = TRUE) + public function fetch_required_migrations(array $group, $target = TRUE) { $migrations = array(); $current_groups = $this->fetch_groups(TRUE); - foreach ( (array) $group as $group_name) + // Make sure the group(s) exist + foreach ($group as $group_name) { if ( ! isset($current_groups[$group_name])) { @@ -303,25 +304,46 @@ class Model_Minion_Migration extends Model $up = $target; // If we want to limit this migration to certain groups - if ($group !== NULL) + if ( ! empty($group)) { - if (is_array($group)) + if (count($group) > 1) { $query->where('group', 'IN', $group); } else { - $query->where('group', '=', $group); + $query->where('group', '=', $group[0]); } } } - else + // Relative up/down target + elseif (in_array($target[0], array('+', '-'))) { list($target, $up) = $this->resolve_target($group, $target); $query->where('group', '=', $group); - if ($up) + if( $target !== NULL) + { + if ($up) + { + $query->where('timestamp', '<=', $target); + } + else + { + $query->where('timestamp', '>=', $target); + } + } + + } + // Absolute timestamp + else + { + $query->where('group', '=', $group); + + $statuses = $this->fetch_current_versions(); + + if ($up = ($statuses[$group[0]] < $target)) { $query->where('timestamp', '<=', $target); } @@ -342,12 +364,13 @@ class Model_Minion_Migration extends Model else { $query - ->where('applied', '>', 0) + ->where('applied', '=', 1) ->order_by('timestamp', 'DESC'); } + //var_dump($query->compile($this->_db)); - return $query->execute($this->_db)->as_array(); + return array($query->execute($this->_db)->as_array(), $up); } /** @@ -374,82 +397,56 @@ class Model_Minion_Migration extends Model $group = $group[0]; } - $amount = NULL; + if( ! in_array($target[0], array('+', '-'))) + { + throw new Kohana_Exception("Invalid relative target"); + } + $query = $this->_select(); $statuses = $this->fetch_current_versions(); $target = (string) $target; $group_applied = isset($statuses[$group]); - $up = NULL; $timestamp = $group_applied ? $statuses[$group]['timestamp'] : NULL; - - // If this target is relative to the current state of the group - if ($target[0] === '+' OR $target[0] === '-') - { - $amount = substr($target, 1); - $up = $target[0] === '+'; - - if ($up) - { - if ($group_applied) - { - $query->where('timestamp', '>', $timestamp); - } - } - else - { - if ( ! $group_applied) - { - throw new Kohana_Exception( - "Cannot migrate group :group down as none of its migrations have been applied", - array(':group' => $group) - ); - } - - $query->where('timestamp', '<=', $timestamp); - } - - $query->limit($amount); - } - // Else this is an absolute target - else + $amount = substr($target, 1); + $up = $target[0] === '+'; + + if ($up) { if ($group_applied) { - $up = ( (int) $timestamp < (int) $target); - - $query->where('timestamp', ($up ? '>' : '<='), $timestamp); - } - else - { - $up = TRUE; - } - - if ($up) - { - $query->where('timestamp', '<=', $target); - } - else - { - $query->where('timestamp', '>', $target); + $query->where('timestamp', '>', $timestamp); } } - - if ( ! $up) + else { - $query->where('applied', '>', 0); + if ( ! $group_applied) + { + throw new Kohana_Exception( + "Cannot migrate group :group down as none of its migrations have been applied", + array(':group' => $group) + ); + } + + $query + ->where('applied', '=', 1) + ->where('timestamp', '<=', $timestamp); } + $query->limit($amount); + $query->where('group', '=', $group); $query->order_by('timestamp', ($up ? 'ASC' : 'DESC')); $results = $query->execute($this->_db); + //var_dump($query->compile($this->_db)); + if ($amount !== NULL AND count($results) != $amount) { return array(NULL, $up); } - return array((float) $query->execute($this->_db)->get('timestamp'), $up); + return array((string) $query->execute($this->_db)->get('timestamp'), $up); } } diff --git a/tests/datasets/minion/migration/model.xml b/tests/datasets/minion/migration/model.xml index c50e780..da73bf1 100644 --- a/tests/datasets/minion/migration/model.xml +++ b/tests/datasets/minion/migration/model.xml @@ -8,4 +8,9 @@ + + + + + diff --git a/tests/minion/migration/model.php b/tests/minion/migration/model.php index 6e5797f..6bbf4dd 100644 --- a/tests/minion/migration/model.php +++ b/tests/minion/migration/model.php @@ -66,7 +66,7 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase { $migrations = $this->getModel()->fetch_all(); - $this->assertSame(7, count($migrations)); + $this->assertSame(10, count($migrations)); } /** @@ -84,6 +84,7 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase array ( 'app' => '20101216080000', 'dblogger' => '20101225000000', + 'done' => '20101546112100', ), $versions ); @@ -101,19 +102,22 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase array( array( array( - 'timestamp' => '20101215165000', - 'description' => 'add-name-column-to-members', - 'group' => 'app', - 'applied' => '0', - 'id' => 'app:20101215165000', - ), - array( - 'timestamp' => '20101216000000', - 'description' => 'add-index-on-name', - 'group' => 'app', - 'applied' => '0', - 'id' => 'app:20101216000000', + array( + 'timestamp' => '20101215165000', + 'description' => 'add-name-column-to-members', + 'group' => 'app', + 'applied' => '0', + 'id' => 'app:20101215165000', + ), + array( + 'timestamp' => '20101216000000', + 'description' => 'add-index-on-name', + 'group' => 'app', + 'applied' => '0', + 'id' => 'app:20101216000000', + ), ), + TRUE ), 'app', TRUE, @@ -122,19 +126,22 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase array( array( array( - 'timestamp' => '20101216080000', - 'description' => 'remove-password-salt-column', - 'group' => 'app', - 'applied' => '1', - 'id' => 'app:20101216080000', - ), - array( - 'timestamp' => '20101215164400', - 'description' => 'create-tables', - 'group' => 'app', - 'applied' => '1', - 'id' => 'app:20101215164400', + array( + 'timestamp' => '20101216080000', + 'description' => 'remove-password-salt-column', + 'group' => 'app', + 'applied' => '1', + 'id' => 'app:20101216080000', + ), + array( + 'timestamp' => '20101215164400', + 'description' => 'create-tables', + 'group' => 'app', + 'applied' => '1', + 'id' => 'app:20101215164400', + ), ), + FALSE ), 'app', FALSE @@ -143,26 +150,36 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase array( array( array( - 'timestamp' => '20101215165000', - 'description' => 'add-name-column-to-members', - 'group' => 'app', - 'applied' => '0', - 'id' => 'app:20101215165000', - ), - array( - 'timestamp' => '20101216000000', - 'description' => 'add-index-on-name', - 'group' => 'app', - 'applied' => '0', - 'id' => 'app:20101216000000', - ), - array( - 'timestamp' => '20101226112100', - 'description' => 'add-pk', - 'group' => 'dblogger', - 'applied' => '0', - 'id' => 'dblogger:20101226112100', + array( + 'timestamp' => '20101215165000', + 'description' => 'add-name-column-to-members', + 'group' => 'app', + 'applied' => '0', + 'id' => 'app:20101215165000', + ), + array( + 'timestamp' => '20101216000000', + 'description' => 'add-index-on-name', + 'group' => 'app', + 'applied' => '0', + 'id' => 'app:20101216000000', + ), + array( + 'timestamp' => '20101226112100', + 'description' => 'add-pk', + 'group' => 'dblogger', + 'applied' => '0', + 'id' => 'dblogger:20101226112100', + ), + array( + 'timestamp' => '20101526112100', + 'description' => 'add-col', + 'group' => 'minion', + 'applied' => '0', + 'id' => 'minion:20101526112100', + ), ), + TRUE ), NULL, TRUE @@ -171,57 +188,104 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase array( array( array( - 'timestamp' => '20101225000000', - 'description' => 'remove-unique-index', - 'group' => 'dblogger', - 'applied' => '1', - 'id' => 'dblogger:20101225000000', - ), - array( - 'timestamp' => '20101216080000', - 'description' => 'remove-password-salt-column', - 'group' => 'app', - 'applied' => '1', - 'id' => 'app:20101216080000', - ), - array( - 'timestamp' => '20101215164500', - 'description' => 'create-table', - 'group' => 'dblogger', - 'applied' => '1', - 'id' => 'dblogger:20101215164500', - ), - array( - 'timestamp' => '20101215164400', - 'description' => 'create-tables', - 'group' => 'app', - 'applied' => '1', - 'id' => 'app:20101215164400', + array( + 'timestamp' => '20101546112100', + 'description' => 'add-bbb', + 'group' => 'done', + 'applied' => '1', + 'id' => 'done:20101546112100' + ), + array( + 'timestamp' => '20101536112100', + 'description' => 'add-aaa', + 'group' => 'done', + 'applied' => '1', + 'id'=> 'done:20101536112100' + ), + array( + 'timestamp' => '20101225000000', + 'description' => 'remove-unique-index', + 'group' => 'dblogger', + 'applied' => '1', + 'id' => 'dblogger:20101225000000', + ), + array( + 'timestamp' => '20101216080000', + 'description' => 'remove-password-salt-column', + 'group' => 'app', + 'applied' => '1', + 'id' => 'app:20101216080000', + ), + array( + 'timestamp' => '20101215164500', + 'description' => 'create-table', + 'group' => 'dblogger', + 'applied' => '1', + 'id' => 'dblogger:20101215164500', + ), + array( + 'timestamp' => '20101215164400', + 'description' => 'create-tables', + 'group' => 'app', + 'applied' => '1', + 'id' => 'app:20101215164400', + ), ), + FALSE, ), NULL, FALSE ), + // Test migrating down to a specific version array( array( array( - 'timestamp' => '20101216080000', - 'description' => 'remove-password-salt-column', - 'group' => 'app', - 'applied' => '1', - 'id' => 'app:20101216080000', - ), - array( - 'timestamp' => '20101215164400', - 'description' => 'create-tables', - 'group' => 'app', - 'applied' => '1', - 'id' => 'app:20101215164400', + array( + 'timestamp' => '20101216080000', + 'description' => 'remove-password-salt-column', + 'group' => 'app', + 'applied' => '1', + 'id' => 'app:20101216080000', + ), ), + FALSE ), 'app', - 20101216080000 + 20101215164400 ), + // Test migrating up from nothing to everything + array( + array( + array( + array( + 'timestamp' => '20101526112100', + 'description' => 'add-col', + 'group' => 'minion', + 'applied' => '0', + 'id' => 'minion:20101526112100', + ), + ), + TRUE + ), + 'minion', + '+100' + ), + array( + array( + array( + array( + 'timestamp' => '20101546112100', + 'description' => 'add-bbb', + 'group' => 'done', + 'applied' => '1', + 'id' => 'done:20101546112100' + ), + ), + FALSE + ), + 'done', + '-1' + ) ); } @@ -239,7 +303,7 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase { $this->assertSame( $expected, - $this->getModel()->fetch_required_migrations($group, $target) + $this->getModel()->fetch_required_migrations((array) $group, $target) ); } @@ -385,7 +449,7 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase { return array( array( - array(20101216080000, FALSE), + array('20101216080000', FALSE), 'app', '-1' ), @@ -395,7 +459,7 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase '-10' ), array( - array(20101226112100, TRUE), + array('20101226112100', TRUE), 'dblogger', '+1', ), @@ -428,7 +492,7 @@ class Minion_Migration_ModelTest extends Kohana_Unittest_Database_TestCase { $this->assertSame( $expected, - $this->getModel()->resolve_target($group, $target) + $this->getModel()->resolve_target((array) $group, $target) ); } } diff --git a/views/minion/task/db/status.php b/views/minion/task/db/status.php index e6f0170..f9a17f4 100644 --- a/views/minion/task/db/status.php +++ b/views/minion/task/db/status.php @@ -1,4 +1,4 @@ $status): ?> - * + *