also_migrate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +18 -0
- data/README.markdown +32 -0
- data/Rakefile +2 -0
- data/init.rb +1 -0
- data/lib/also_migrate.rb +6 -0
- data/lib/also_migrate/base.rb +30 -0
- data/lib/also_migrate/migration.rb +65 -0
- data/lib/also_migrate/migrator.rb +108 -0
- data/log/development.log +6 -0
- data/rails/init.rb +2 -0
- data/require.rb +52 -0
- data/spec/Rakefile +10 -0
- data/spec/also_migrate_spec.rb +53 -0
- data/spec/config/database.yml.example +6 -0
- data/spec/db/migrate/001_create_articles.rb +14 -0
- data/spec/db/migrate/002_add_permalink.rb +9 -0
- data/spec/db/migrate/003_remove_ignored.rb +9 -0
- data/spec/fixtures/article.rb +3 -0
- data/spec/log/test.log +6455 -0
- data/spec/spec_helper.rb +27 -0
- metadata +83 -0
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
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/rails/init"
|
data/lib/also_migrate.rb
ADDED
@@ -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
|
data/log/development.log
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
[4;36;1mSQL (0.2ms)[0m [0;1mSET SQL_AUTO_IS_NULL=0[0m
|
2
|
+
[4;35;1mUser Load (59.6ms)[0m [0mSELECT * FROM `users` ORDER BY users.id DESC LIMIT 1[0m
|
3
|
+
[4;36;1mUser Columns (21.6ms)[0m [0;1mSHOW FIELDS FROM `users`[0m
|
4
|
+
[4;36;1mSQL (0.3ms)[0m [0;1mSET SQL_AUTO_IS_NULL=0[0m
|
5
|
+
[4;35;1mSQL (0.3ms)[0m [0mSHOW TABLES[0m
|
6
|
+
[4;36;1mUser Columns (13.0ms)[0m [0;1mSHOW FIELDS FROM `users`[0m
|
data/rails/init.rb
ADDED
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,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
|