also_migrate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Winton Welsh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,32 @@
1
+ AlsoMigrate
2
+ ===========
3
+
4
+ Migrate multiple tables with similar schema.
5
+
6
+ Requirements
7
+ ------------
8
+
9
+ <pre>
10
+ sudo gem install also_migrate
11
+ </pre>
12
+
13
+ Define the model
14
+ ----------------
15
+
16
+ <pre>
17
+ class Article < ActiveRecord::Base
18
+ also_migrate :article_archives, :ignore => 'moved_at', :indexes => 'id'
19
+ end
20
+ </pre>
21
+
22
+ Options:
23
+
24
+ * <code>:ignore</code> Ignore migrations that apply to certain columns (defaults to none)
25
+ * <code>:indexes</code> Only index certain columns (defaults to all)
26
+
27
+ That's it!
28
+ ----------
29
+
30
+ Next time you migrate, <code>article_archives</code> is created if it doesn't exist.
31
+
32
+ Any new migration applied to <code>articles</code> is automatically applied to <code>article_archives</code>.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "#{File.dirname(__FILE__)}/require"
2
+ Require.rakefile!
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,6 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.lib!
3
+
4
+ ActiveRecord::Base.send(:include, AlsoMigrate::Base)
5
+ ActiveRecord::Migrator.send(:include, AlsoMigrate::Migrator)
6
+ ActiveRecord::Migration.send(:include, AlsoMigrate::Migration)
@@ -0,0 +1,30 @@
1
+ module AlsoMigrate
2
+ module Base
3
+
4
+ def self.included(base)
5
+ unless base.respond_to?(:also_migrate)
6
+ base.extend ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def also_migrate(*args)
13
+ options = args.extract_options!
14
+ @also_migrate_config ||= []
15
+ @also_migrate_config << {
16
+ :tables => args.collect(&:to_s),
17
+ :options => {
18
+ :ignore => [ options[:ignore] ].flatten.compact,
19
+ :indexes => options[:indexes] ? [ options[:indexes] ].flatten : nil
20
+ }
21
+ }
22
+ self.class_eval do
23
+ class <<self
24
+ attr_accessor :also_migrate_config
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,65 @@
1
+ module AlsoMigrate
2
+ module Migration
3
+
4
+ def self.included(base)
5
+ unless base.respond_to?(:method_missing_with_also_migrate)
6
+ base.extend ClassMethods
7
+ base.class_eval do
8
+ class <<self
9
+ alias_method :method_missing_without_also_migrate, :method_missing
10
+ alias_method :method_missing, :method_missing_with_also_migrate
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ def method_missing_with_also_migrate(method, *arguments, &block)
19
+ args = Marshal.load(Marshal.dump(arguments))
20
+ method_missing_without_also_migrate(method, *arguments, &block)
21
+
22
+ supported = [
23
+ :add_column, :add_index, :add_timestamps, :change_column,
24
+ :change_column_default, :change_table, :create_table,
25
+ :drop_table, :remove_column, :remove_columns,
26
+ :remove_timestamps, :rename_column, :rename_table
27
+ ]
28
+
29
+ if !args.empty? && supported.include?(method)
30
+ connection = ActiveRecord::Base.connection
31
+ table_name = ActiveRecord::Migrator.proper_table_name(args[0])
32
+
33
+ # Find models
34
+ Object.subclasses_of(ActiveRecord::Base).each do |klass|
35
+ if klass.respond_to?(:also_migrate_config)
36
+ next unless klass.table_name == table_name
37
+ klass.also_migrate_config.each do |config|
38
+ options = config[:options]
39
+ tables = config[:tables]
40
+
41
+ # Don't change ignored columns
42
+ options[:ignore].each do |column|
43
+ next if args.include?(column) || args.include?(column.intern)
44
+ end
45
+
46
+ # Run migration
47
+ config[:tables].each do |table|
48
+ if method == :create_table
49
+ ActiveRecord::Migrator::AlsoMigrate.create_tables(klass)
50
+ elsif method == :add_index && !options[:indexes].nil?
51
+ next
52
+ elsif connection.table_exists?(table)
53
+ args[0] = table
54
+ args[1] = table if method == :rename_table
55
+ connection.send(method, *args, &block)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,108 @@
1
+ module AlsoMigrate
2
+ module Migrator
3
+
4
+ def self.included(base)
5
+ unless base.respond_to?(:migrate_with_also_migrate)
6
+ base.send :include, InstanceMethods
7
+ base.class_eval do
8
+ alias_method :migrate_without_also_migrate, :migrate
9
+ alias_method :migrate, :migrate_with_also_migrate
10
+ end
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+
16
+ def migrate_with_also_migrate
17
+ Object.subclasses_of(ActiveRecord::Base).each do |klass|
18
+ if klass.respond_to?(:also_migrate_config)
19
+ AlsoMigrate.create_tables(klass)
20
+ end
21
+ end
22
+ ensure
23
+ migrate_without_also_migrate
24
+ end
25
+
26
+ module AlsoMigrate
27
+ class <<self
28
+
29
+ def connection
30
+ ActiveRecord::Base.connection
31
+ end
32
+
33
+ def create_tables(klass)
34
+ config = klass.also_migrate_config
35
+ old_table = klass.table_name
36
+ config.each do |config|
37
+ options = config[:options]
38
+ config[:tables].each do |new_table|
39
+ if !connection.table_exists?(new_table) && connection.table_exists?(old_table)
40
+ columns = connection.columns(old_table).collect(&:name)
41
+ columns -= options[:ignore].collect(&:to_s)
42
+ columns.collect! { |col| connection.quote_column_name(col) }
43
+ engine =
44
+ if connection.class.to_s.include?('Mysql')
45
+ 'ENGINE=' + connection.select_one(<<-SQL)['Engine']
46
+ SHOW TABLE STATUS
47
+ WHERE Name = '#{old_table}'
48
+ SQL
49
+ end
50
+ connection.execute(<<-SQL)
51
+ CREATE TABLE #{new_table} #{engine}
52
+ AS SELECT #{columns.join(',')}
53
+ FROM #{old_table}
54
+ WHERE false;
55
+ SQL
56
+ indexes = options[:indexes]
57
+ indexes ||= indexed_columns(old_table)
58
+ indexes.each do |column|
59
+ connection.add_index(new_table, column)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def indexed_columns(table_name)
67
+ # MySQL
68
+ if connection.class.to_s.include?('Mysql')
69
+ index_query = "SHOW INDEX FROM #{table_name}"
70
+ connection.select_all(index_query).collect do |r|
71
+ r["Column_name"]
72
+ end
73
+ # PostgreSQL
74
+ # http://stackoverflow.com/questions/2204058/show-which-columns-an-index-is-on-in-postgresql/2213199
75
+ elsif connection.class.to_s.include?('PostgreSQL')
76
+ index_query = <<-SQL
77
+ select
78
+ t.relname as table_name,
79
+ i.relname as index_name,
80
+ a.attname as column_name
81
+ from
82
+ pg_class t,
83
+ pg_class i,
84
+ pg_index ix,
85
+ pg_attribute a
86
+ where
87
+ t.oid = ix.indrelid
88
+ and i.oid = ix.indexrelid
89
+ and a.attrelid = t.oid
90
+ and a.attnum = ANY(ix.indkey)
91
+ and t.relkind = 'r'
92
+ and t.relname = '#{table_name}'
93
+ order by
94
+ t.relname,
95
+ i.relname
96
+ SQL
97
+ connection.select_all(index_query).collect do |r|
98
+ r["column_name"]
99
+ end
100
+ else
101
+ raise 'AlsoMigrate does not support this database adapter'
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,6 @@
1
+ SQL (0.2ms) SET SQL_AUTO_IS_NULL=0
2
+ User Load (59.6ms) SELECT * FROM `users` ORDER BY users.id DESC LIMIT 1
3
+ User Columns (21.6ms) SHOW FIELDS FROM `users`
4
+ SQL (0.3ms) SET SQL_AUTO_IS_NULL=0
5
+ SQL (0.3ms) SHOW TABLES
6
+ User Columns (13.0ms) SHOW FIELDS FROM `users`
data/rails/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.rails_init!
data/require.rb ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ gem 'require'
3
+ require 'require'
4
+
5
+ Require do
6
+ gem(:active_wrapper, '=0.2.3') { require 'active_wrapper' }
7
+ gem :require, '=0.2.6'
8
+ gem(:rake, '=0.8.7') { require 'rake' }
9
+ gem :rspec, '=1.3.0'
10
+
11
+ gemspec do
12
+ author 'Winton Welsh'
13
+ dependencies do
14
+ gem :require
15
+ end
16
+ email 'mail@wintoni.us'
17
+ name 'also_migrate'
18
+ homepage "http://github.com/winton/#{name}"
19
+ summary "Migrate multiple tables with similar schema"
20
+ version '0.1.0'
21
+ end
22
+
23
+ bin { require 'lib/also_migrate' }
24
+
25
+ lib do
26
+ require 'lib/also_migrate/base'
27
+ require 'lib/also_migrate/migration'
28
+ require 'lib/also_migrate/migrator'
29
+ end
30
+
31
+ rails_init { require 'lib/also_migrate' }
32
+
33
+ rakefile do
34
+ gem(:active_wrapper)
35
+ gem(:rake) { require 'rake/gempackagetask' }
36
+ gem(:rspec) { require 'spec/rake/spectask' }
37
+ require 'require/tasks'
38
+ end
39
+
40
+ spec_helper do
41
+ gem(:active_wrapper)
42
+ require 'require/spec_helper'
43
+ require 'rails/init'
44
+ require 'pp'
45
+ require 'spec/fixtures/article'
46
+ end
47
+
48
+ spec_rakefile do
49
+ gem(:rake)
50
+ gem(:active_wrapper) { require 'active_wrapper/tasks' }
51
+ end
52
+ end
data/spec/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.spec_rakefile!
3
+
4
+ begin
5
+ ActiveWrapper::Tasks.setup(
6
+ :base => File.dirname(__FILE__),
7
+ :env => 'test'
8
+ )
9
+ rescue Exception
10
+ end
@@ -0,0 +1,53 @@
1
+ require "spec_helper"
2
+
3
+ describe AlsoMigrate do
4
+
5
+ describe 'fixture config' do
6
+
7
+ before(:each) do
8
+ $db.migrate(1)
9
+ $db.migrate(0)
10
+ $db.migrate(1)
11
+ end
12
+
13
+ it 'should migrate both tables up' do
14
+ migrate_with_state(2)
15
+ (@new_article_columns - @old_article_columns).should == [ 'permalink' ]
16
+ (@new_archive_columns - @old_archive_columns).should == [ 'permalink' ]
17
+ end
18
+
19
+ it 'should migrate both tables down' do
20
+ $db.migrate(2)
21
+ migrate_with_state(1)
22
+ (@old_article_columns - @new_article_columns).should == [ 'permalink' ]
23
+ (@old_archive_columns - @new_archive_columns).should == [ 'permalink' ]
24
+ end
25
+
26
+ it "should ignore the body column column" do
27
+ (columns('articles') - columns('article_archives')).should == [ 'body' ]
28
+ connection.remove_column(:articles, :body)
29
+ (columns('articles') - columns('article_archives')).should == []
30
+ end
31
+
32
+ it "should only add an index for id" do
33
+ ActiveRecord::Migrator::AlsoMigrate.indexed_columns('articles').should == [ 'id', 'read' ]
34
+ ActiveRecord::Migrator::AlsoMigrate.indexed_columns('article_archives').should == [ 'id' ]
35
+ end
36
+ end
37
+
38
+ describe 'no index config' do
39
+
40
+ before(:each) do
41
+ Article.also_migrate_config = nil
42
+ Article.also_migrate :article_archives
43
+ $db.migrate(1)
44
+ $db.migrate(0)
45
+ $db.migrate(1)
46
+ end
47
+
48
+ it "should add all indexes" do
49
+ ActiveRecord::Migrator::AlsoMigrate.indexed_columns('articles').should == [ 'id', 'read' ]
50
+ ActiveRecord::Migrator::AlsoMigrate.indexed_columns('article_archives').should == [ 'id', 'read' ]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ adapter: mysql
3
+ database: also_migrate
4
+ username: root
5
+ password:
6
+ host: localhost
@@ -0,0 +1,14 @@
1
+ class CreateArticles < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :articles do |t|
4
+ t.string :title
5
+ t.string :body
6
+ t.boolean :read
7
+ end
8
+ add_index :articles, :read
9
+ end
10
+
11
+ def self.down
12
+ drop_table :articles
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ class AddPermalink < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :articles, :permalink, :string
4
+ end
5
+
6
+ def self.down
7
+ remove_column :articles, :permalink
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class RemoveMagicColumns < ActiveRecord::Migration
2
+ def self.up
3
+ remove_column :articles, :move_id
4
+ remove_column :articles, :moved_at
5
+ end
6
+
7
+ def self.down
8
+ end
9
+ end