├── MIT-LICENSE ├── README ├── Rakefile ├── init.rb ├── lib └── auto_migrations.rb ├── tasks └── auto_migrations_tasks.rake └── test └── auto_migrations_test.rb /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 PJ Hyett 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == AutoMigrations 2 | 3 | Forget migrations, auto-migrate! 4 | 5 | 6 | == Usage 7 | 8 | Write out your schema (or use an existing one) 9 | 10 | $ cat db/schema.rb 11 | 12 | ActiveRecord::Schema.define do 13 | 14 | create_table :posts do |t| 15 | t.string :title 16 | t.text :body 17 | t.timestamps 18 | end 19 | 20 | end 21 | 22 | $ rake db:auto:migrate 23 | 24 | Created posts table 25 | 26 | ...a few days later 27 | 28 | $ cat db/schema.rb 29 | 30 | ActiveRecord::Schema.define do 31 | 32 | create_table :posts do |t| 33 | t.string :title 34 | t.text :content 35 | t.timestamps 36 | end 37 | 38 | end 39 | 40 | $ rake db:auto:migrate 41 | -- add_column("posts", :content, :text) 42 | -> 0.0307s 43 | -- remove_column("posts", "body") 44 | -> 0.0311s 45 | 46 | * PJ Hyett [ pjhyett@gmail.com ] 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the auto_migrations plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the auto_migrations plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | files = ['README', 'LICENSE', 'lib/**/*.rb'] 18 | rdoc.rdoc_files.add(files) 19 | rdoc.main = "README" # page to start on 20 | rdoc.title = "auto_migrations" 21 | rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 22 | rdoc.rdoc_dir = 'doc' # rdoc output folder 23 | rdoc.options << '--inline-source' 24 | end 25 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'auto_migrations' 2 | ActiveRecord::Migration.send :include, AutoMigrations 3 | -------------------------------------------------------------------------------- /lib/auto_migrations.rb: -------------------------------------------------------------------------------- 1 | module AutoMigrations 2 | 3 | def self.run 4 | # Turn off schema_info code for auto-migration 5 | class << ActiveRecord::Schema 6 | alias :old_define :define 7 | attr_accessor :version 8 | def define(info={}, &block) @version = Time.now.utc.strftime("%Y%m%d%H%M%S"); instance_eval(&block) end 9 | end 10 | 11 | load(File.join(RAILS_ROOT, 'db', 'schema.rb')) 12 | ActiveRecord::Migration.drop_unused_tables 13 | ActiveRecord::Migration.drop_unused_indexes 14 | ActiveRecord::Migration.update_schema_version(ActiveRecord::Schema.version) if ActiveRecord::Schema.version 15 | 16 | class << ActiveRecord::Schema 17 | alias :define :old_define 18 | end 19 | end 20 | 21 | def self.schema_to_migration(with_reset = false) 22 | schema_in = File.read(File.join(RAILS_ROOT, "db", "schema.rb")) 23 | schema_in.gsub!(/#(.)+\n/, '') 24 | schema_in.sub!(/ActiveRecord::Schema.define(.+)do[ ]?\n/, '') 25 | schema_in.gsub!(/^/, ' ') 26 | schema = "class InitialSchema < ActiveRecord::Migration\n def self.up\n" 27 | schema += " # We're resetting the migrations database...\n" + 28 | " drop_table :schema_migrations\n" + 29 | " initialize_schema_migrations_table\n\n" if with_reset 30 | schema += schema_in 31 | schema << "\n def self.down\n" 32 | schema << (ActiveRecord::Base.connection.tables - %w(schema_info schema_migrations)).map do |table| 33 | " drop_table :#{table}\n" 34 | end.join 35 | schema << " end\nend\n" 36 | migration_file = File.join(RAILS_ROOT, "db", "migrate", "001_initial_schema.rb") 37 | File.open(migration_file, "w") { |f| f << schema } 38 | puts "Migration created at db/migrate/001_initial_schema.rb" 39 | end 40 | 41 | def self.included(base) 42 | base.extend ClassMethods 43 | class << base 44 | cattr_accessor :tables_in_schema, :indexes_in_schema 45 | self.tables_in_schema, self.indexes_in_schema = [], [] 46 | alias_method_chain :method_missing, :auto_migration 47 | end 48 | end 49 | 50 | module ClassMethods 51 | 52 | def method_missing_with_auto_migration(method, *args, &block) 53 | case method 54 | when :create_table 55 | auto_create_table(method, *args, &block) 56 | when :add_index 57 | auto_add_index(method, *args, &block) 58 | else 59 | method_missing_without_auto_migration(method, *args, &block) 60 | end 61 | end 62 | 63 | def auto_create_table(method, *args, &block) 64 | table_name = args.shift.to_s 65 | options = args.pop || {} 66 | 67 | (self.tables_in_schema ||= []) << table_name 68 | 69 | # Table doesn't exist, create it 70 | unless ActiveRecord::Base.connection.tables.include?(table_name) 71 | return method_missing_without_auto_migration(method, *[table_name, options], &block) 72 | end 73 | 74 | # Grab database columns 75 | fields_in_db = ActiveRecord::Base.connection.columns(table_name).inject({}) do |hash, column| 76 | hash[column.name] = column 77 | hash 78 | end 79 | 80 | # Grab schema columns (lifted from active_record/connection_adapters/abstract/schema_statements.rb) 81 | table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(ActiveRecord::Base.connection) 82 | primary_key = options[:primary_key] || "id" 83 | table_definition.primary_key(primary_key) unless options[:id] == false 84 | yield table_definition 85 | fields_in_schema = table_definition.columns.inject({}) do |hash, column| 86 | hash[column.name.to_s] = column 87 | hash 88 | end 89 | 90 | # Add fields to db new to schema 91 | (fields_in_schema.keys - fields_in_db.keys).each do |field| 92 | column = fields_in_schema[field] 93 | options = {:limit => column.limit, :precision => column.precision, :scale => column.scale} 94 | options[:default] = column.default if !column.default.nil? 95 | options[:null] = column.null if !column.null.nil? 96 | add_column table_name, column.name, column.type.to_sym, options 97 | end 98 | 99 | # Remove fields from db no longer in schema 100 | (fields_in_db.keys - fields_in_schema.keys & fields_in_db.keys).each do |field| 101 | column = fields_in_db[field] 102 | remove_column table_name, column.name 103 | end 104 | 105 | (fields_in_schema.keys & fields_in_db.keys).each do |field| 106 | if field != primary_key #ActiveRecord::Base.get_primary_key(table_name) 107 | changed = false # flag 108 | new_type = fields_in_schema[field].type.to_sym 109 | new_attr = {} 110 | 111 | # First, check if the field type changed 112 | if fields_in_schema[field].type.to_sym != fields_in_db[field].type.to_sym 113 | changed = true 114 | end 115 | 116 | # Special catch for precision/scale, since *both* must be specified together 117 | # Always include them in the attr struct, but they'll only get applied if changed = true 118 | new_attr[:precision] = fields_in_schema[field][:precision] 119 | new_attr[:scale] = fields_in_schema[field][:scale] 120 | 121 | # Next, iterate through our extended attributes, looking for any differences 122 | # This catches stuff like :null, :precision, etc 123 | fields_in_schema[field].each_pair do |att,value| 124 | next if att == :type or att == :base or att == :name # special cases 125 | if !value.nil? && value != fields_in_db[field].send(att) 126 | new_attr[att] = value 127 | changed = true 128 | end 129 | end 130 | 131 | # Change the column if applicable 132 | change_column table_name, field, new_type, new_attr if changed 133 | end 134 | end 135 | end 136 | 137 | def auto_add_index(method, *args, &block) 138 | table_name = args.shift.to_s 139 | fields = Array(args.shift).map(&:to_s) 140 | options = args.shift 141 | 142 | index_name = options[:name] if options 143 | index_name ||= ActiveRecord::Base.connection.index_name(table_name, :column => fields) 144 | 145 | (self.indexes_in_schema ||= []) << index_name 146 | 147 | unless ActiveRecord::Base.connection.indexes(table_name).detect { |i| i.name == index_name } 148 | method_missing_without_auto_migration(method, *[table_name, fields, options], &block) 149 | end 150 | end 151 | 152 | def drop_unused_tables 153 | (ActiveRecord::Base.connection.tables - tables_in_schema - %w(schema_info schema_migrations)).each do |table| 154 | drop_table table 155 | end 156 | end 157 | 158 | def drop_unused_indexes 159 | tables_in_schema.each do |table_name| 160 | indexes_in_db = ActiveRecord::Base.connection.indexes(table_name).map(&:name) 161 | (indexes_in_db - indexes_in_schema & indexes_in_db).each do |index_name| 162 | remove_index table_name, :name => index_name 163 | end 164 | end 165 | end 166 | 167 | def update_schema_version(version) 168 | if ActiveRecord::Base.connection.tables.include?("schema_migrations") 169 | ActiveRecord::Base.connection.update("INSERT INTO schema_migrations VALUES ('#{version}')") 170 | end 171 | schema_file = File.join(RAILS_ROOT, "db", "schema.rb") 172 | schema = File.read(schema_file) 173 | schema.sub!(/:version => \d+/, ":version => #{version}") 174 | File.open(schema_file, "w") { |f| f << schema } 175 | end 176 | 177 | end 178 | 179 | end 180 | -------------------------------------------------------------------------------- /tasks/auto_migrations_tasks.rake: -------------------------------------------------------------------------------- 1 | namespace :db do 2 | namespace :auto do 3 | desc "Use schema.rb to auto-migrate" 4 | task :migrate => :environment do 5 | AutoMigrations.run 6 | end 7 | end 8 | 9 | namespace :schema do 10 | desc "Create migration from schema.rb" 11 | task :to_migration => :environment do 12 | AutoMigrations.schema_to_migration 13 | end 14 | 15 | desc "Create migration from schema.rb and reset migrations log" 16 | task :to_migration_with_reset => :environment do 17 | AutoMigrations.schema_to_migration(true) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/auto_migrations_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class AutoMigrationsTest < Test::Unit::TestCase 4 | # Replace this with your real tests. 5 | def test_this_plugin 6 | flunk 7 | end 8 | end 9 | --------------------------------------------------------------------------------