Skip to content

Commit

Permalink
Added migration parts functionality.
Browse files Browse the repository at this point in the history
  • Loading branch information
mlightner committed Nov 9, 2010
0 parents commit a27ab71
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,20 @@
Copyright (c) 2007 PJ Hyett

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.
46 changes: 46 additions & 0 deletions README
@@ -0,0 +1,46 @@
== AutoMigrations

Forget migrations, auto-migrate!


== Usage

Write out your schema (or use an existing one)

$ cat db/schema.rb

ActiveRecord::Schema.define do

create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end

end

$ rake db:auto:migrate

Created posts table

...a few days later

$ cat db/schema.rb

ActiveRecord::Schema.define do

create_table :posts do |t|
t.string :title
t.text :content
t.timestamps
end

end

$ rake db:auto:migrate
-- add_column("posts", :content, :text)
-> 0.0307s
-- remove_column("posts", "body")
-> 0.0311s

* PJ Hyett [ pjhyett@gmail.com ]
24 changes: 24 additions & 0 deletions Rakefile
@@ -0,0 +1,24 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'

desc 'Default: run unit tests.'
task :default => :test

desc 'Test the auto_migrations plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end

desc 'Generate documentation for the auto_migrations plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
files = ['README', 'LICENSE', 'lib/**/*.rb']
rdoc.rdoc_files.add(files)
rdoc.main = "README" # page to start on
rdoc.title = "auto_migrations"
rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb"
rdoc.rdoc_dir = 'doc' # rdoc output folder
rdoc.options << '--inline-source'
end
2 changes: 2 additions & 0 deletions init.rb
@@ -0,0 +1,2 @@
require 'auto_migrations'
ActiveRecord::Migration.send :include, AutoMigrations
191 changes: 191 additions & 0 deletions lib/auto_migrations.rb
@@ -0,0 +1,191 @@
module AutoMigrations

def self.run
# Turn off schema_info code for auto-migration
class << ActiveRecord::Schema
alias :old_define :define
attr_accessor :version
def define(info={}, &block)
@version = Time.now.utc.strftime("%Y%m%d%H%M%S")
if part = info.delete(:part)
announce "Loading schema component: #{part.to_s.humanize}"
end
instance_eval(&block)
end
end

load(File.join(RAILS_ROOT, 'db', 'schema.rb'))
Dir.glob(File.join(RAILS_ROOT + '/db/schema_parts/*.rb')).each do |file|
if !(`/usr/bin/env ruby -c #{file}` =~ /Syntax OK/m)
warn "Invalid syntax in: #{file}"
next
end
load(file)
end
ActiveRecord::Migration.drop_unused_tables
ActiveRecord::Migration.drop_unused_indexes
ActiveRecord::Migration.update_schema_version(ActiveRecord::Schema.version) if ActiveRecord::Schema.version

class << ActiveRecord::Schema
alias :define :old_define
end
end

def self.schema_to_migration(with_reset = false)
schema_in = File.read(File.join(RAILS_ROOT, "db", "schema.rb"))
schema_in.gsub!(/#(.)+\n/, '')
schema_in.sub!(/ActiveRecord::Schema.define(.+)do[ ]?\n/, '')
schema_in.gsub!(/^/, ' ')
schema = "class InitialSchema < ActiveRecord::Migration\n def self.up\n"
schema += " # We're resetting the migrations database...\n" +
" drop_table :schema_migrations\n" +
" initialize_schema_migrations_table\n\n" if with_reset
schema += schema_in
schema << "\n def self.down\n"
schema << (ActiveRecord::Base.connection.tables - %w(schema_info schema_migrations)).map do |table|
" drop_table :#{table}\n"
end.join
schema << " end\nend\n"
migration_file = File.join(RAILS_ROOT, "db", "migrate", "001_initial_schema.rb")
File.open(migration_file, "w") { |f| f << schema }
puts "Migration created at db/migrate/001_initial_schema.rb"
end

def self.included(base)
base.extend ClassMethods
class << base
cattr_accessor :tables_in_schema, :indexes_in_schema
self.tables_in_schema, self.indexes_in_schema = [], []
alias_method_chain :method_missing, :auto_migration
end
end

module ClassMethods

def method_missing_with_auto_migration(method, *args, &block)
case method
when :create_table
auto_create_table(method, *args, &block)
when :add_index
auto_add_index(method, *args, &block)
else
method_missing_without_auto_migration(method, *args, &block)
end
end

def auto_create_table(method, *args, &block)
table_name = args.shift.to_s
options = args.pop || {}

(self.tables_in_schema ||= []) << table_name

# Table doesn't exist, create it
unless ActiveRecord::Base.connection.tables.include?(table_name)
return method_missing_without_auto_migration(method, *[table_name, options], &block)
end

# Grab database columns
fields_in_db = ActiveRecord::Base.connection.columns(table_name).inject({}) do |hash, column|
hash[column.name] = column
hash
end

# Grab schema columns (lifted from active_record/connection_adapters/abstract/schema_statements.rb)
table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(ActiveRecord::Base.connection)
primary_key = options[:primary_key] || "id"
table_definition.primary_key(primary_key) unless options[:id] == false
yield table_definition
fields_in_schema = table_definition.columns.inject({}) do |hash, column|
hash[column.name.to_s] = column
hash
end

# Add fields to db new to schema
(fields_in_schema.keys - fields_in_db.keys).each do |field|
column = fields_in_schema[field]
options = {:limit => column.limit, :precision => column.precision, :scale => column.scale}
options[:default] = column.default if !column.default.nil?
options[:null] = column.null if !column.null.nil?
add_column table_name, column.name, column.type.to_sym, options
end

# Remove fields from db no longer in schema
(fields_in_db.keys - fields_in_schema.keys & fields_in_db.keys).each do |field|
column = fields_in_db[field]
remove_column table_name, column.name
end

(fields_in_schema.keys & fields_in_db.keys).each do |field|
if field != primary_key #ActiveRecord::Base.get_primary_key(table_name)
changed = false # flag
new_type = fields_in_schema[field].type.to_sym
new_attr = {}

# First, check if the field type changed
if fields_in_schema[field].type.to_sym != fields_in_db[field].type.to_sym
changed = true
end

# Special catch for precision/scale, since *both* must be specified together
# Always include them in the attr struct, but they'll only get applied if changed = true
new_attr[:precision] = fields_in_schema[field][:precision]
new_attr[:scale] = fields_in_schema[field][:scale]

# Next, iterate through our extended attributes, looking for any differences
# This catches stuff like :null, :precision, etc
fields_in_schema[field].each_pair do |att,value|
next if att == :type or att == :base or att == :name # special cases
if !value.nil? && value != fields_in_db[field].send(att)
new_attr[att] = value
changed = true
end
end

# Change the column if applicable
change_column table_name, field, new_type, new_attr if changed
end
end
end

def auto_add_index(method, *args, &block)
table_name = args.shift.to_s
fields = Array(args.shift).map(&:to_s)
options = args.shift

index_name = options[:name] if options
index_name ||= ActiveRecord::Base.connection.index_name(table_name, :column => fields)

(self.indexes_in_schema ||= []) << index_name

unless ActiveRecord::Base.connection.indexes(table_name).detect { |i| i.name == index_name }
method_missing_without_auto_migration(method, *[table_name, fields, options], &block)
end
end

def drop_unused_tables
(ActiveRecord::Base.connection.tables - tables_in_schema - %w(schema_info schema_migrations)).each do |table|
drop_table table
end
end

def drop_unused_indexes
tables_in_schema.each do |table_name|
indexes_in_db = ActiveRecord::Base.connection.indexes(table_name).map(&:name)
(indexes_in_db - indexes_in_schema & indexes_in_db).each do |index_name|
remove_index table_name, :name => index_name
end
end
end

def update_schema_version(version)
ActiveRecord::Base.connection.update("INSERT INTO schema_migrations VALUES ('#{version}')")

schema_file = File.join(RAILS_ROOT, "db", "schema.rb")
schema = File.read(schema_file)
schema.sub!(/:version => \d+/, ":version => #{version}")
File.open(schema_file, "w") { |f| f << schema }
end

end

end
20 changes: 20 additions & 0 deletions tasks/auto_migrations_tasks.rake
@@ -0,0 +1,20 @@
namespace :db do
namespace :auto do
desc "Use schema.rb to auto-migrate"
task :migrate => :environment do
AutoMigrations.run
end
end

namespace :schema do
desc "Create migration from schema.rb"
task :to_migration => :environment do
AutoMigrations.schema_to_migration
end

desc "Create migration from schema.rb and reset migrations log"
task :to_migration_with_reset => :environment do
AutoMigrations.schema_to_migration(true)
end
end
end
8 changes: 8 additions & 0 deletions test/auto_migrations_test.rb
@@ -0,0 +1,8 @@
require 'test/unit'

class AutoMigrationsTest < Test::Unit::TestCase
# Replace this with your real tests.
def test_this_plugin
flunk
end
end

0 comments on commit a27ab71

Please sign in to comment.