├── classes ├── Migration.php ├── Flexiblemigrations.php ├── Model │ └── Migration.php ├── Task │ ├── Db │ │ ├── Rollback.php │ │ └── Migrate.php │ └── Generate │ │ └── Migration.php ├── Controller │ └── Flexiblemigrations.php ├── Drivers │ ├── Driver.php │ └── Mysql.php └── Kohana │ └── Flexiblemigrations.php ├── migrations.sql ├── views ├── flexiblemigrations │ ├── rollback.php │ ├── migrate.php │ ├── new.php │ └── index.php ├── migrations.php └── migration_template.php ├── config └── flexiblemigrations.php ├── MIT-LICENSE ├── init.php └── README.markdown /classes/Migration.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ferpetrelli/kohana-flexible-migrations/HEAD/classes/Migration.php -------------------------------------------------------------------------------- /classes/Flexiblemigrations.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ERROR 7 | 8 | 9 | 10 | uri() , "
Back"); ?> 11 | -------------------------------------------------------------------------------- /config/flexiblemigrations.php: -------------------------------------------------------------------------------- 1 | TRUE, 7 | 8 | //Route path to web frontend 9 | 'web_frontend_route' => 'migrations', 10 | 11 | //Path where migration files are going to be generated 12 | 'path' => APPPATH . 'migrations/' 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /views/migrations.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 |
13 | 14 | 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /views/flexiblemigrations/migrate.php: -------------------------------------------------------------------------------- 1 | 2 | Nothing to migrate 3 | 4 | 5 | 6 | 7 | OK 8 | 9 | 10 | ERROR 11 | 12 | 13 | 14 | 15 | uri() , "
Back"); ?> -------------------------------------------------------------------------------- /views/flexiblemigrations/new.php: -------------------------------------------------------------------------------- 1 | get_once('message',false); ?> 2 | 3 |
4 | 5 | 6 |
Create New Migration
7 | 8 |
9 | 10 | 11 |
12 | 13 |
14 |
Please use only alphanumeric characters and spaces, and don't use php reserved words
15 | -------------------------------------------------------------------------------- /views/migration_template.php: -------------------------------------------------------------------------------- 1 | defined('SYSPATH') or die('No direct script access.'); 2 | 3 | class extends Migration 4 | { 5 | public function up() 6 | { 7 | // $this->create_table 8 | // ( 9 | // 'table_name', 10 | // array 11 | // ( 12 | // 'updated_at' => array('datetime'), 13 | // 'created_at' => array('datetime'), 14 | // ) 15 | // ); 16 | 17 | // $this->add_column('table_name', 'column_name', array('datetime', 'default' => NULL)); 18 | } 19 | 20 | public function down() 21 | { 22 | // $this->drop_table('table_name'); 23 | 24 | // $this->remove_column('table_name', 'column_name'); 25 | } 26 | } -------------------------------------------------------------------------------- /classes/Model/Migration.php: -------------------------------------------------------------------------------- 1 | array('type' => 'int'), 7 | 'hash' => array('type' => 'string'), 8 | 'name' => array('type' => 'string'), 9 | 'updated_at' => array('type' => 'datetime'), 10 | 'created_at' => array('type' => 'datetime'), 11 | ); 12 | 13 | public function is_installed() { 14 | try { 15 | $this->count_all(); 16 | } catch (Database_Exception $a) { 17 | return false; 18 | } 19 | return true; 20 | } 21 | 22 | public function fetch_migrations() { 23 | try { 24 | $this->find_all(); 25 | } catch (Database_Exception $a) { 26 | if ($a->getCode() == 1146) { //Tabla no existe 27 | echo kohana::debug($a->getMessage()); 28 | } 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Fernando Petrelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /classes/Task/Db/Rollback.php: -------------------------------------------------------------------------------- 1 | 2 | rollback(); 25 | 26 | if (empty($messages)) { 27 | Minion_CLI::write("There's no migration to rollback"); 28 | } else { 29 | foreach ($messages as $message) { 30 | if (key($message) == 0) { 31 | Minion_CLI::write($message[0]); 32 | } else { 33 | Minion_CLI::write($message[key($message)]); 34 | Minion_CLI::write("ERROR"); 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /classes/Task/Db/Migrate.php: -------------------------------------------------------------------------------- 1 | 2 | migrate(); 25 | 26 | if (empty($messages)) { 27 | Minion_CLI::write("Nothing to migrate"); 28 | } else { 29 | foreach ($messages as $message) { 30 | if (key($message) == 0) { 31 | Minion_CLI::write($message[0]); 32 | Minion_CLI::write("OK"); 33 | } else { 34 | Minion_CLI::write($message[key($message)]); 35 | Minion_CLI::write("ERROR"); 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /classes/Task/Generate/Migration.php: -------------------------------------------------------------------------------- 1 | NULL, 7 | ); 8 | 9 | public function build_validation(Validation $validation) 10 | { 11 | return parent::build_validation($validation) 12 | ->rule('name', 'not_empty'); 13 | } 14 | 15 | /** 16 | * Task to build a new migration file 17 | * 18 | * @return null 19 | */ 20 | protected function _execute(array $params) 21 | { 22 | $migrations = new Flexiblemigrations(TRUE); 23 | try 24 | { 25 | $model = ORM::factory('Migration'); 26 | } 27 | catch (Database_Exception $a) 28 | { 29 | Minion_CLI::write('Flexible Migrations is not installed. Please Run the migrations.sql script in your mysql server'); 30 | exit(); 31 | } 32 | 33 | $status = $migrations->generate_migration($params['name']); 34 | 35 | if ($status == 0) 36 | { 37 | Minion_CLI::write('Migration ' . $params['name'] . ' was succefully created'); 38 | Minion_CLI::write('Please check migrations folder'); 39 | } 40 | else 41 | { 42 | Minion_CLI::write('There was an error while creating migration ' . $params['name']); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | get_config(); 5 | 6 | // Enabling the Userguide module from my Module 7 | // Kohana::modules(Kohana::modules() + array('userguide' => MODPATH.'userguide')); 8 | 9 | //If web frontend is enabled, set the routes 10 | if ($migrations_config['web_frontend']) 11 | { 12 | Route::set('migrations_route',$migrations_config['web_frontend_route']) 13 | ->defaults(array( 14 | 'controller' => 'flexiblemigrations', 15 | 'action' => 'index', 16 | )); 17 | 18 | Route::set('migrations_new',$migrations_config['web_frontend_route'] . '/new') 19 | ->defaults(array( 20 | 'controller' => 'flexiblemigrations', 21 | 'action' => 'new', 22 | )); 23 | 24 | Route::set('migrations_create',$migrations_config['web_frontend_route'] . '/create') 25 | ->defaults(array( 26 | 'controller' => 'flexiblemigrations', 27 | 'action' => 'create', 28 | )); 29 | 30 | Route::set('migrations_migrate',$migrations_config['web_frontend_route'] . '/migrate') 31 | ->defaults(array( 32 | 'controller' => 'flexiblemigrations', 33 | 'action' => 'migrate', 34 | )); 35 | 36 | Route::set('migrations_rollback',$migrations_config['web_frontend_route'] . '/rollback') 37 | ->defaults(array( 38 | 'controller' => 'flexiblemigrations', 39 | 'action' => 'rollback', 40 | )); 41 | } 42 | -------------------------------------------------------------------------------- /views/flexiblemigrations/index.php: -------------------------------------------------------------------------------- 1 |

Flexible Migrations

2 | 3 | get_once('message',false); ?> 4 | 5 |
6 | 7 | 8 |
9 | uri() , 'Generate NEW migration') ?> 10 |
11 |
12 | 13 |
14 | uri() , 'RUN ALL PENDING MIGRATIONS') ?> 15 |
16 | 17 |
18 | uri() , 'ROLLBACK') ?> 19 |
20 | 21 |

List of migrations

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | $migration) { ?> 31 | 32 | 33 | 40 | 41 | 42 | 43 |
MigrationStatus
34 | 35 | OK 36 | 37 | PENDING 38 | 39 |
44 | 45 |
46 | 47 |
48 | uri() , 'Generate NEW migration') ?> 49 |
-------------------------------------------------------------------------------- /classes/Controller/Flexiblemigrations.php: -------------------------------------------------------------------------------- 1 | migrations = new Flexiblemigrations(TRUE); 26 | try 27 | { 28 | $this->model = ORM::factory('Migration'); 29 | } 30 | catch (Database_Exception $a) 31 | { 32 | echo 'Flexible Migrations is not installed. Please Run the migrations.sql script in your mysql server'; 33 | exit(); 34 | } 35 | 36 | parent::before(); 37 | } 38 | 39 | public function action_index() 40 | { 41 | $migrations=$this->migrations->get_migrations(); 42 | rsort($migrations); 43 | 44 | //Get migrations already runned from the DB 45 | $migrations_runned = ORM::factory('Migration')->find_all()->as_array('hash'); 46 | 47 | $this->view = new View('flexiblemigrations/index'); 48 | $this->view->set_global('migrations', $migrations); 49 | $this->view->set_global('migrations_runned', $migrations_runned); 50 | 51 | $this->template->view = $this->view; 52 | } 53 | 54 | public function action_new() 55 | { 56 | $this->view = new View('flexiblemigrations/new'); 57 | $this->template->view = $this->view; 58 | } 59 | 60 | public function action_create() 61 | { 62 | $migration_name = str_replace(' ','_',$_REQUEST['migration_name']); 63 | $session = Session::instance(); 64 | 65 | try 66 | { 67 | if (empty($migration_name)) 68 | throw new Exception("Migration mame must not be empty"); 69 | 70 | $this->migrations->generate_migration($migration_name); 71 | 72 | //Sets a status message 73 | $session->set('message', "Migration ".$migration_name." was succefully created. Check migrations folder"); 74 | } 75 | catch (Exception $e) 76 | { 77 | $session->set('message', $e->getMessage()); 78 | } 79 | 80 | $this->redirect(Route::get('migrations_route')->uri()); 81 | } 82 | 83 | public function action_migrate() 84 | { 85 | $this->view = new View('flexiblemigrations/migrate'); 86 | $this->template->view = $this->view; 87 | 88 | $messages = $this->migrations->migrate(); 89 | $this->view->set_global('messages', $messages); 90 | } 91 | 92 | public function action_rollback() 93 | { 94 | $this->view = new View('flexiblemigrations/rollback'); 95 | $this->template->view = $this->view; 96 | 97 | $messages = $this->migrations->rollback(); 98 | $this->view->set_global('messages', $messages); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /classes/Drivers/Driver.php: -------------------------------------------------------------------------------- 1 | group = $group; 39 | $this->db = $db; 40 | $this->types = array_combine($this->types, $this->types); 41 | } 42 | 43 | /** 44 | * Create Table 45 | * 46 | * Creates a new table 47 | * 48 | * $fields: 49 | * 50 | * Associative array containing the name of the field as a key and the 51 | * value could be either a string indicating the type of the field, or an 52 | * array containing the field type at the first position and any optional 53 | * arguments the field might require in the remaining positions. 54 | * Refer to the TYPES function for valid type arguments. 55 | * Refer to the FIELD_ARGUMENTS function for valid optional arguments for a 56 | * field. 57 | * 58 | * @example 59 | * 60 | * create_table ( 61 | * 'blog', 62 | * array ( 63 | * 'title' => array ( 'string[50]', 'default' => "The blog's title." ), 64 | * 'date' => 'date', 65 | * 'content' => 'text' 66 | * ) 67 | * ) 68 | * 69 | * @param string Name of the table to be created 70 | * @param array 71 | * @param mixed Primary key, false if not desired, not specified sets to 'id' column. 72 | * Will be set to auto_increment by default. 73 | * @return boolean 74 | */ 75 | abstract public function create_table($table_name, $fields, $primary_key = TRUE); 76 | 77 | /** 78 | * Drop a table 79 | * 80 | * @param string Name of the table 81 | * @return boolean 82 | */ 83 | abstract public function drop_table($table_name); 84 | 85 | /** 86 | * Rename a table 87 | * 88 | * @param string Old table name 89 | * @param string New name 90 | * @return boolean 91 | */ 92 | abstract public function rename_table($old_name, $new_name); 93 | 94 | /** 95 | * Add a column to a table 96 | * 97 | * @example add_column ( "the_table", "the_field", array('string[25]', 'null' => FALSE) ); 98 | * @example add_coumnn ( "the_table", "int_field", "integer" ); 99 | * 100 | * @param string Name of the table 101 | * @param string Name of the column 102 | * @param array Column arguments array, or just a type for a simple column 103 | * @return bool 104 | */ 105 | abstract public function add_column($table_name, $column_name, $params); 106 | 107 | /** 108 | * Rename a column 109 | * 110 | * @param string Name of the table 111 | * @param string Name of the column 112 | * @param string New name 113 | * @return bool 114 | */ 115 | abstract public function rename_column($table_name, $column_name, $new_column_name, $params); 116 | 117 | /** 118 | * Alter a column 119 | * 120 | * @param string Table name 121 | * @param string Columnn ame 122 | * @param string New column type 123 | * @param array Column argumetns 124 | * @return bool 125 | */ 126 | abstract public function change_column($table_name, $column_name, $params); 127 | 128 | /** 129 | * Remove a column from a table 130 | * 131 | * @param string Name of the table 132 | * @param string Name of the column 133 | * @return bool 134 | */ 135 | abstract public function remove_column($table_name, $column_name); 136 | 137 | /** 138 | * Add an index 139 | * 140 | * @param string Name of the table 141 | * @param string Name of the index 142 | * @param string|array Name(s) of the column(s) 143 | * @param string Type of the index (unique/normal/primary) 144 | * @return bool 145 | */ 146 | abstract public function add_index($table_name, $index_name, $columns, $index_type = 'normal'); 147 | 148 | /** 149 | * Remove an index 150 | * 151 | * @param string Name of the table 152 | * @param string Name of the index 153 | * @return bool 154 | */ 155 | abstract public function remove_index($table_name, $index_name); 156 | 157 | /** 158 | * Catch query exceptions 159 | * 160 | * @return bool 161 | */ 162 | public function run_query($sql) 163 | { 164 | $test = $this->db->query($this->group, $sql, false); 165 | } 166 | 167 | /** 168 | * Is this a valid type? 169 | * 170 | * @return bool 171 | */ 172 | protected function is_type($type) 173 | { 174 | return isset($this->types[$type]); 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## Kohana Flexible Migrations 2 | 3 | Kohana Flexible migrations is a Rails inspired migration module for kohana. 4 | 5 | It's based on kohana-migrations module by Matías Montes and Jamie Madill. 6 | 7 | Some features: 8 | 9 | * Kohana 3.0, 3.1, 3.2 and 3.3 compatibility 10 | * Automatic migration file generation 11 | * Minion tasks to generate migrations, execute and rollback them. 12 | * Solves the problem with migrations numbers using a timestamp instead of a integer 13 | * Supports migrations and rollbacks 14 | * Due the naming convention for files, you can work on teams without concerns! 15 | * Nice web interface 16 | 17 | ## Compatibility 18 | 19 | **Kohana 3.3** 'master' branch 20 | 21 | **Kohana 3.2** download 'kohana_3.2' branch 22 | 23 | **Kohana 3.1** download 'kohana_3.1' branch 24 | 25 | **Kohana 3.0** download 'kohana_3.0' branch 26 | 27 | NOTE: Minion tasks doesn't work using kohana 3.0 28 | 29 | ## Installation 30 | 31 | 1) Download module, and copy the folder to your modules directory. (e.g. modules/flexiblemigrations) 32 | 33 | 2) Enable flexiblemigrations and orm modules in bootstrap.php 34 | 35 | ```php 36 | 'flexiblemigrations' => MODPATH.'flexiblemigrations' 37 | 'orm' => MODPATH.'orm' 38 | ``` 39 | 40 | 3) Run the migrations.sql script on your DB server 41 | 42 | 4) Create and grant WRITE privileges to /application/migrations folder 43 | 44 | ## Configuration (optional) 45 | 46 | You can set some useful options inside config/flexiblemigrations.php file: 47 | 48 | - Enable/Disable web frontend (to use only Minion tasks) 49 | ```php 50 | 'web_frontend' => TRUE 51 | ``` 52 | 53 | - If web frontend is enabled you can change their route 54 | ```php 55 | 'web_frontend_route' => 'migrations', 56 | ``` 57 | 58 | - Path where migration files are going to be generated 59 | ```php 60 | 'path' => APPPATH . 'migrations/' 61 | ``` 62 | 63 | ## Usage 64 | 65 | **COMMAND LINE INTERFACE (Minion tasks):** Go ahead to sub-section "Minion tasks" 66 | 67 | **WEB FRONTEND:** Go to url: yoursite/migrations (or your route if you've changed it on config/flexiblemigrations.php file) and that's it! you will see a nice web interface with a menu. 68 | 69 | 1) Click on 'Generate NEW migration' you can set a name and create a new migration file. 70 | 71 | E.g. for a migration called 'typical migration' generated file could be: 72 | 73 | ``` 74 | 20120526170715_typical_migration.php 75 | ``` 76 | 77 | 2) Edit your file and add the DB functions. (Remember to properly set up() and down() functions) 78 | 79 | 80 | A typical migration file looks like: 81 | 82 | ```php 83 | create_table 89 | ( 90 | 'table_name', 91 | array 92 | ( 93 | 'published' => array('boolean'), 94 | 'published_at' => array('datetime'), 95 | 'user_id' => array('integer', 'unsigned' => TRUE), 96 | 'points' => array('integer'), 97 | 'image_file_name' => array('string[255]'), 98 | 'full_description' => array('text'), 99 | ) 100 | ); 101 | 102 | $this->add_column('another_table_name', 'column_name', array('datetime', 'default' => NULL)); 103 | } 104 | 105 | public function down() 106 | { 107 | $this->drop_table('table_name'); 108 | $this->remove_column('another_table_name', 'column_name'); 109 | } 110 | } 111 | ?> 112 | ``` 113 | 114 | 3) When you finish editing, simply click on 'Run all pending migrations'. If you made a mistake, click on 'Rollback'. 115 | 116 | *Enjoy!* 117 | 118 | ## Minion tasks - CLI 119 | 120 | NOTE: Works only on kohana 3.1, 3.2, and 3.3 versions. 121 | 122 | To generate a new migration: 123 | ``` 124 | ./minion generate:migration --name=MIGRATION_NAME 125 | ``` 126 | 127 | To run all pending migrations 128 | ``` 129 | ./minion db:migrate 130 | ``` 131 | 132 | To rollback last executed migration 133 | ``` 134 | ./minion db:rollback 135 | ``` 136 | 137 | ## Migration functions 138 | 139 | All possible functions are: 140 | 141 | ```php 142 | create_table($table_name, $fields, $primary_key = TRUE) 143 | drop_table($table_name) 144 | rename_table($old_name, $new_name) 145 | 146 | add_column($table_name, $column_name, $params) 147 | rename_column($table_name, $column_name, $new_column_name) 148 | change_column($table_name, $column_name, $params) 149 | remove_column($table_name, $column_name) 150 | 151 | add_index($table_name, $index_name, $columns, $index_type = 'normal') 152 | remove_index($table_name, $index_name) 153 | ``` 154 | 155 | There's a new function to run custom SQL. I discourage to use this, but it's useful in some edge cases. Use it at your own risk :) 156 | ```php 157 | sql($sql_consult) 158 | ``` 159 | 160 | Possible DB columns datatypes are: * 161 | 162 | ``` 163 | text 164 | string[NumberOfCharacters] 165 | decimal 166 | integer 167 | datetime 168 | date 169 | boolean 170 | float 171 | timestamp 172 | time 173 | binary 174 | ``` 175 | 176 | In all cases you can pass a default value (see file example above) 177 | 178 | ## To do 179 | 180 | * Full documentation 181 | * Code refactor 182 | * Improve minion tasks 183 | * Support several DB engines (Postgre, Oracle, etc) 184 | 185 | ## Contact 186 | 187 | *All your contributions are welcome!!! Just make a pull request* 188 | 189 | To get some help or give suggestions please contact the author. 190 | 191 | ## License 192 | 193 | MIT License. Copyright 2012-2014 Fernando Petrelli. 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /classes/Kohana/Flexiblemigrations.php: -------------------------------------------------------------------------------- 1 | _config = Kohana::$config->load('flexiblemigrations')->as_array(); 25 | } 26 | 27 | public function get_config() 28 | { 29 | return $this->_config; 30 | } 31 | 32 | /** 33 | * Run all pending migrations 34 | * 35 | */ 36 | public function migrate() 37 | { 38 | $migration_keys = $this->get_migration_keys(); 39 | $migrations = ORM::factory('migration')->find_all(); 40 | $messages = array(); 41 | 42 | //Remove executed migrations from queue 43 | foreach ($migrations as $migration) 44 | { 45 | if (array_key_exists($migration->hash, $migration_keys)) 46 | { 47 | unset($migration_keys[$migration->hash]); 48 | } 49 | } 50 | 51 | if (count($migration_keys) > 0) 52 | { 53 | foreach ($migration_keys as $key => $value) 54 | { 55 | $msg = "Executing migration: '" . $value . "' with hash: " .$key; 56 | 57 | try 58 | { 59 | $migration_object = $this->load_migration($key); 60 | $migration_object->up(); 61 | $model = ORM::factory('migration'); 62 | $model->hash = $key; 63 | $model->name = $value; 64 | $model->save(); 65 | $model ? $messages[] = array(0 => $msg) : $messages[] = array(1 => $msg); 66 | } 67 | catch (Exception $e) 68 | { 69 | $messages[] = array(1 => $msg . "\n" . $e->getMessage()); 70 | return $messages; 71 | } 72 | } 73 | } 74 | return $messages; 75 | } 76 | 77 | /** 78 | * Rollback last executed migration. 79 | * 80 | */ 81 | public function rollback() 82 | { 83 | //Get last executed migration 84 | $model = ORM::factory('migration')->order_by('created_at','DESC')->order_by('hash','DESC')->limit(1)->find(); 85 | $messages = array(); 86 | 87 | if ($model->loaded()) 88 | { 89 | try 90 | { 91 | $migration_object = $this->load_migration($model->hash); 92 | $migration_object->down(); 93 | 94 | if ($model) 95 | { 96 | $msg = "Migration '" . $model->name . "' with hash: " . $model->hash . ' was succefully "rollbacked"'; 97 | $messages[] = array(0 => $msg); 98 | } else { 99 | $messages[] = array(1 => "Error executing rollback"); 100 | } 101 | $model->delete(); 102 | } 103 | catch (Exception $e) 104 | { 105 | $messages[] = array(1 => $e->getMessage()); 106 | } 107 | } 108 | else 109 | { 110 | $messages[] = array(1 => "There's no migration to rollback"); 111 | } 112 | 113 | return $messages; 114 | } 115 | 116 | /** 117 | * Rollback last executed migration. 118 | * 119 | */ 120 | public function get_timestamp() 121 | { 122 | return date('YmdHis'); 123 | } 124 | 125 | /** 126 | * Get all valid migrations file names 127 | * 128 | * @return array migrations_filenames 129 | */ 130 | public function get_migrations() 131 | { 132 | $migrations = glob($this->_config['path'].'*'.EXT); 133 | foreach ($migrations as $i => $file) 134 | { 135 | $name = basename($file, EXT); 136 | if (!preg_match('/^\d{14}_(\w+)$/', $name)) //Check filename format 137 | unset($migrations[$i]); 138 | } 139 | sort($migrations); 140 | return $migrations; 141 | } 142 | 143 | /** 144 | * Generates a new migration file 145 | * TODO: Probably needs to be in outer class 146 | * 147 | * @return integer completion_code 148 | */ 149 | public function generate_migration($migration_name) 150 | { 151 | try 152 | { 153 | //Creates the migration file with the timestamp and the name from params 154 | $file_name = $this->get_timestamp(). '_' . $migration_name . '.php'; 155 | $config = $this->get_config(); 156 | $file = fopen($config['path'].$file_name, 'w+'); 157 | 158 | //Opens the template file and replaces the name 159 | $view = new View('migration_template'); 160 | $view->set_global('migration_name', $migration_name); 161 | fwrite($file, $view); 162 | fclose($file); 163 | chmod($config['path'].$file_name, 0770); 164 | return 0; 165 | } 166 | catch (Exception $e) 167 | { 168 | return 1; 169 | } 170 | } 171 | 172 | /** 173 | * Get all migration keys (timestamps) 174 | * 175 | * @return array migrations_keys 176 | */ 177 | protected function get_migration_keys() 178 | { 179 | $migrations = $this->get_migrations(); 180 | $keys = array(); 181 | foreach ($migrations as $migration) 182 | { 183 | $sub_migration = substr(basename($migration, EXT), 0, 14); 184 | $keys = Arr::merge($keys, array($sub_migration => substr(basename($migration, EXT), 15))); 185 | } 186 | return $keys; 187 | } 188 | 189 | /** 190 | * Load the migration file, and returns a Migration object 191 | * 192 | * @return Migration object with up and down functions 193 | */ 194 | protected function load_migration($version) 195 | { 196 | $f = glob($this->_config['path'].$version.'*'. EXT); 197 | 198 | if ( count($f) > 1 ) // Only one migration per step is permitted 199 | throw new Kohana_Exception('There are repeated migration names'); 200 | 201 | if ( count($f) == 0 ) // Migration step not found 202 | throw new Kohana_Exception("There's no migration to rollback"); 203 | 204 | $file = basename($f[0]); 205 | $name = basename($f[0], EXT); 206 | 207 | // Filename validation 208 | if ( !preg_match('/^\d{14}_(\w+)$/', $name, $match) ) 209 | throw new Kohana_Exception('Invalid filename :file', array(':file' => $file)); 210 | 211 | $match[1] = strtolower($match[1]); 212 | require $f[0]; //Includes migration class file 213 | 214 | $class = ucfirst($match[1]); //Get the class name capitalized 215 | 216 | if ( !class_exists($class) ) 217 | throw new Kohana_Exception('Class :class doesn\'t exists', array(':class' => $class)); 218 | 219 | if ( !method_exists($class, 'up') OR !method_exists($class, 'down') ) 220 | throw new Kohana_Exception('Up or down functions missing on class :class', array(':class' => $class)); 221 | 222 | return new $class(); 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /classes/Drivers/Mysql.php: -------------------------------------------------------------------------------- 1 | db->query($group, 'START TRANSACTION', false); 9 | } 10 | 11 | public function __destruct() 12 | { 13 | $this->db->query($this->group, 'COMMIT', false); 14 | } 15 | 16 | public function create_table($table_name, $fields, $primary_key = TRUE) 17 | { 18 | $sql = "CREATE TABLE `$table_name` ("; 19 | 20 | // add a default id column if we don't say not to 21 | if ($primary_key === TRUE) 22 | { 23 | $primary_key = 'id'; 24 | $fields = array_merge(array('id' => array('integer', 'null' => FALSE)), $fields); 25 | } 26 | 27 | foreach ($fields as $field_name => $params) 28 | { 29 | $params = (array) $params; 30 | 31 | if ($primary_key === $field_name AND $params[0] == 'integer') 32 | { 33 | $params['auto'] = TRUE; 34 | } 35 | 36 | $sql .= $this->compile_column($field_name, $params); 37 | $sql .= ","; 38 | } 39 | 40 | $sql = rtrim($sql, ','); 41 | 42 | if ($primary_key) 43 | { 44 | $sql .= ' , PRIMARY KEY ('; 45 | 46 | foreach ( (array) $primary_key as $pk ) { 47 | $sql .= " `$pk`,"; 48 | } 49 | $sql = rtrim($sql, ','); 50 | $sql .= ')'; 51 | } 52 | 53 | $sql .= ")"; 54 | 55 | return $this->run_query($sql); 56 | } 57 | 58 | public function drop_table($table_name) 59 | { 60 | return $this->run_query("DROP TABLE $table_name"); 61 | } 62 | 63 | public function rename_table($old_name, $new_name) 64 | { 65 | return $this->run_query("RENAME TABLE `$old_name` TO `$new_name` ;"); 66 | } 67 | 68 | public function add_column($table_name, $column_name, $params) 69 | { 70 | $sql = "ALTER TABLE `$table_name` ADD COLUMN " . $this->compile_column($column_name, $params, TRUE); 71 | return $this->run_query($sql); 72 | } 73 | 74 | public function rename_column($table_name, $column_name, $new_column_name, $params) 75 | { 76 | if ($params == NULL) { 77 | $params = $this->get_column($table_name, $column_name); 78 | } 79 | $sql = "ALTER TABLE `$table_name` CHANGE `$column_name` " . $this->compile_column($new_column_name, $params, TRUE); 80 | return $this->run_query($sql); 81 | } 82 | 83 | public function change_column($table_name, $column_name, $params) 84 | { 85 | $sql = "ALTER TABLE `$table_name` MODIFY " . $this->compile_column($column_name, $params); 86 | return $this->run_query($sql); 87 | } 88 | 89 | public function remove_column($table_name, $column_name) 90 | { 91 | return $this->run_query("ALTER TABLE $table_name DROP COLUMN $column_name ;"); 92 | } 93 | 94 | public function add_index($table_name, $index_name, $columns, $index_type = 'normal') 95 | { 96 | switch ($index_type) 97 | { 98 | case 'normal': $type = 'INDEX'; break; 99 | case 'unique': $type = 'UNIQUE KEY'; break; 100 | case 'primary': $type = 'PRIMARY KEY'; break; 101 | 102 | default: throw new Kohana_Exception('migrations.bad_index_type :index_type', array(':index_type' => $index_type)); 103 | } 104 | 105 | $sql = "ALTER TABLE `$table_name` ADD $type `$index_name` ("; 106 | 107 | foreach ((array) $columns as $column) 108 | { 109 | $sql .= " `$column`,"; 110 | } 111 | 112 | $sql = rtrim($sql, ','); 113 | $sql .= ')'; 114 | return $this->run_query($sql); 115 | } 116 | 117 | public function remove_index($table_name, $index_name) 118 | { 119 | return $this->run_query("ALTER TABLE `$table_name` DROP INDEX `$index_name`"); 120 | } 121 | 122 | protected function compile_column($field_name, $params, $allow_order = FALSE) 123 | { 124 | if (empty($params)) 125 | { 126 | throw new Kohana_Exception('migrations.missing_argument'); 127 | } 128 | 129 | $params = (array) $params; 130 | $null = TRUE; 131 | $auto = FALSE; 132 | $unsigned = FALSE; 133 | 134 | foreach ($params as $key => $param) 135 | { 136 | $args = NULL; 137 | 138 | if (is_string($key)) 139 | { 140 | switch ($key) 141 | { 142 | case 'after': if ($allow_order) $order = "AFTER `$param`"; break; 143 | case 'null': $null = (bool) $param; break; 144 | case 'default': 145 | if (is_string($param)) { 146 | $default = 'DEFAULT ' . $this->db->escape($param); 147 | } else if (is_bool($param)) { 148 | if ($param == true) { 149 | $default = 'DEFAULT 1'; 150 | } else { 151 | $default = 'DEFAULT 0'; 152 | } 153 | } else { 154 | $default = 'DEFAULT ' . $param; 155 | } 156 | break; 157 | case 'auto': $auto = (bool) $param; break; 158 | case 'unsigned': $unsigned = $param; break; 159 | default: throw new Kohana_Exception('migrations.bad_column :key', array(':key' => $key)); 160 | } 161 | continue; // next iteration 162 | } 163 | 164 | // Split into param and args 165 | if (is_string($param) AND preg_match('/^([^\[]++)\[(.+)\]$/', $param, $matches)) 166 | { 167 | $param = $matches[1]; 168 | $args = $matches[2]; 169 | 170 | // Replace escaped comma with comma 171 | $args = str_replace('\,', ',', $args); 172 | } 173 | 174 | if ($this->is_type($param)) 175 | { 176 | $type = $this->native_type($param, $args); 177 | continue; 178 | } 179 | 180 | switch ($param) 181 | { 182 | case 'first': if ($allow_order) $order = 'FIRST'; continue 2; 183 | default: break; 184 | } 185 | 186 | throw new Kohana_Exception('migrations.bad_column :column', array(':column' => $field_name)); 187 | } 188 | 189 | if (empty($type)) 190 | { 191 | throw new Kohana_Exception('migrations.missing_argument'); 192 | } 193 | 194 | $sql = " `$field_name` $type "; 195 | 196 | if ($unsigned) { 197 | $sql .= ' UNSIGNED '; 198 | } 199 | isset($default) and $sql .= " $default "; 200 | $sql .= $null ? ' NULL ' : ' NOT NULL '; 201 | $sql .= $auto ? ' AUTO_INCREMENT ' : ''; 202 | isset($order) and $sql .= " $order "; 203 | 204 | return $sql; 205 | } 206 | 207 | protected function get_column($table_name, $column_name) 208 | { 209 | print "SHOW COLUMNS FROM `$table_name` LIKE '$column_name'"; 210 | $result = $this->run_query("SHOW COLUMNS FROM `$table_name` LIKE '$column_name'"); 211 | 212 | 213 | if ($result->count() !== 1) 214 | { 215 | throw new Kohana_Exception('migrations.column_not_found :col_name, :table_name', array(':col_name' => $column_name, ':table_name' => $table_name)); 216 | } 217 | 218 | $result = $result->current(); 219 | $params = array($this->migration_type($result->Type)); 220 | 221 | if ($result->Null == 'NO') 222 | $params['null'] = FALSE; 223 | 224 | if ($result->Default) 225 | $params['default'] = $result->Default; 226 | 227 | if ($result->Extra == 'auto_increment') 228 | $params['auto'] = TRUE; 229 | 230 | return $params; 231 | } 232 | 233 | protected function default_limit($type) 234 | { 235 | switch ($type) 236 | { 237 | case 'decimal': return "10,0"; 238 | case 'integer': return "normal"; 239 | case 'string': return "255"; 240 | case 'binary': return "1"; 241 | case 'boolean': return "1"; 242 | default: return ""; 243 | } 244 | } 245 | 246 | protected function native_type($type, $limit) 247 | { 248 | if (!$this->is_type($type)) 249 | { 250 | throw new Kohana_Exception('migrations.unknown_type :type', array(':type' => $type)); 251 | } 252 | 253 | if (empty($limit)) 254 | { 255 | $limit = $this->default_limit($type); 256 | } 257 | 258 | switch ($type) 259 | { 260 | case 'integer': 261 | switch ($limit) 262 | { 263 | case 'big': return 'bigint'; 264 | case 'normal': return 'int'; 265 | case 'small': return 'smallint'; 266 | default: break; 267 | } 268 | throw new Kohana_Exception('migrations.unknown_type :type', array(':type' => $type)); 269 | 270 | case 'string': return "varchar ($limit)"; 271 | case 'boolean': return 'tinyint (1)'; 272 | default: $limit and $limit = "($limit)"; return "$type $limit"; 273 | } 274 | } 275 | 276 | protected function migration_type($native) 277 | { 278 | if (preg_match('/^([^\(]++)\((.+)\)$/', $native, $matches)) 279 | { 280 | $native = $matches[1]; 281 | $limit = $matches[2]; 282 | } 283 | 284 | switch ($native) 285 | { 286 | case 'bigint': return 'integer[big]'; 287 | case 'smallint': return 'integer[small]'; 288 | case 'int': return 'integer'; 289 | case 'varchar': return "string[$limit]"; 290 | case 'tinyint': return 'boolean'; 291 | default: break; 292 | } 293 | 294 | if (!$this->is_type($native)) 295 | { 296 | throw new Kohana_Exception('migrations.unknown_type :type', array(':type' => $type)); 297 | } 298 | 299 | return $native . "[$limit]"; 300 | } 301 | } 302 | --------------------------------------------------------------------------------