mlins-active_migration 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ module ActiveMigration
2
+ # The keymapper allows you to serialize and deserialize primary keys. This is useful in maintaining foreign key
3
+ # relationships, if you choose not to migrate the primary key.
4
+ #
5
+ # To serialize the keys you simply:
6
+ #
7
+ # write_key_map true
8
+ #
9
+ #
10
+ # To deserialize the key for a foreign key you can specifiy the keymap as the third element:
11
+ #
12
+ # map [['product_id','product_id', :product_migration]]
13
+ # ]
14
+ #
15
+ # If you'd like to access the key from a callback or anywhere outside the mappings array, you can use:
16
+ #
17
+ # mapped_key(:products, old_key)
18
+ #
19
+ module KeyMapper
20
+
21
+ # The path to store the serialized(YAML) keys.
22
+ #
23
+ # Defaults to '/tmp'
24
+ #
25
+ mattr_accessor :storage_path
26
+ self.storage_path = '/tmp'
27
+
28
+ def self.included(base)
29
+ base.class_eval do
30
+ alias_method_chain :run, :key_mapping
31
+ alias_method_chain :migrate_field, :key_mapping
32
+ alias_method_chain :save, :key_mapping
33
+ class << self
34
+ attr_accessor :map_keys
35
+
36
+ # Tells ActiveMigration to serialize the primary key of the legacy model.
37
+ #
38
+ # write_key_map true
39
+ #
40
+ def write_key_map(map_keys)
41
+ @map_keys = map_keys
42
+ end
43
+ alias map_keys= write_key_map
44
+ end
45
+ end
46
+ end
47
+
48
+ def run_with_key_mapping #:nodoc:
49
+ run_without_key_mapping
50
+ serialize_key_map(self.storage_path, self.class.to_s.demodulize.underscore) if self.class.map_keys
51
+ end
52
+
53
+ private
54
+
55
+ def migrate_field_with_key_mapping #:nodoc:
56
+ unless @mapping[2].nil?
57
+ load_keymap(@mapping[2].to_s)
58
+ key = mapped_key(@mapping[2], @legacy_record.instance_eval(@mapping[0]))
59
+ old_value = @legacy_record.__send__(@mapping[0])
60
+ @legacy_record.__send__(@mapping[0] + '=', key)
61
+ end
62
+ migrate_field_without_key_mapping
63
+ @legacy_record.__send__(@mapping[0] + '=', old_value) unless old_value.nil?
64
+ end
65
+
66
+ def save_with_key_mapping #:nodoc:
67
+ save_without_key_mapping
68
+ map_primary_key(@active_record.id, @legacy_record.id) if self.class.map_keys
69
+ end
70
+
71
+ def map_primary_key(active_id, legacy_id) #:nodoc:
72
+ map_name = self.class.to_s.demodulize.underscore
73
+ load_keymap(map_name)
74
+ @maps[map_name] ||= {}
75
+ @maps[map_name][handle_composite(legacy_id)] = handle_composite(active_id)
76
+ end
77
+
78
+ def serialize_key_map(data_path, filename) #:nodoc:
79
+ load_keymap(filename)
80
+ map_name = self.class.to_s.demodulize.underscore
81
+ FileUtils.mkdir_p(data_path)
82
+ FileUtils.rm_rf(File.join(data_path, (filename + "_map.yml")))
83
+ File.open(File.join(data_path, (filename + "_map.yml")), 'w') do |file|
84
+ file.write @maps[map_name].to_yaml
85
+ end
86
+ logger.info("#{self.class.to_s} wrote keymap successfully to #{File.join(data_path, (filename + "_map.yml"))}")
87
+ end
88
+
89
+ # Lazy loader...
90
+ def load_keymap(map) #:nodoc:
91
+ @maps ||= Hash.new
92
+ if @maps[map].nil? && File.file?(File.join(self.storage_path, map.to_s + "_map.yml"))
93
+ @maps[map] = YAML.load(File.open(File.join(self.storage_path, map.to_s + "_map.yml")))
94
+ logger.debug("#{self.class.to_s} lazy loaded #{map} successfully.")
95
+ end
96
+ end
97
+
98
+ # Returns the deserialized mapped key when provided with the former key.
99
+ #
100
+ # mapped_key(:products, 2)
101
+ #
102
+ def mapped_key(map, key)
103
+ load_keymap(map.to_s)
104
+ @maps[map.to_s][handle_composite(key)]
105
+ end
106
+
107
+ # Handles composite primary keys. This assumes you're using the composite_primary_keys gem by Dr. Nic.
108
+ #
109
+ # If id is an array join them with '_' otherwise just return the id.
110
+ #
111
+ def handle_composite(id)
112
+ return id.join('_') if id.is_a?(Array)
113
+ id
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveMigration #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ TINY = 3
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ #--
2
+ # Copyright (c) 2008 Matt Lins
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__)) unless
25
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
+
27
+ unless defined? ActiveSupport
28
+ active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
29
+ if File.exist?(active_support_path)
30
+ $:.unshift active_support_path
31
+ require 'active_support'
32
+ else
33
+ require 'rubygems'
34
+ gem 'activesupport'
35
+ require 'active_support'
36
+ end
37
+ end
38
+
39
+ require 'active_migration/base'
40
+ require 'active_migration/key_mapper'
41
+ require 'active_migration/callbacks'
42
+ require 'active_migration/dependencies'
43
+
44
+ ActiveMigration::Base.class_eval do
45
+ include ActiveMigration::KeyMapper
46
+ include ActiveMigration::Callbacks
47
+ include ActiveMigration::Dependencies
48
+ end
@@ -0,0 +1 @@
1
+ require 'active_migration'
data/spec/base_spec.rb ADDED
@@ -0,0 +1,223 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ require File.dirname(__FILE__) + '/fixtures/product_one_migration'
4
+ require File.dirname(__FILE__) + '/fixtures/product_five_migration'
5
+ require File.dirname(__FILE__) + '/fixtures/product_six_migration'
6
+ require File.dirname(__FILE__) + '/fixtures/product_seven_migration'
7
+ require File.dirname(__FILE__) + '/fixtures/product_eight_migration'
8
+ require File.dirname(__FILE__) + '/fixtures/product_nine_migration'
9
+
10
+ describe 'A migration' do
11
+
12
+ before do
13
+ ActiveMigration::Base.logger = mock('logger', :null_object => true)
14
+ @legacy_record = mock('legacy_model', :id => 1, :name => 'Beer')
15
+ @active_record = mock('active_model', :id => 1, :name= => 'Beer', :save => true)
16
+ @active_record.stub!(:changed?).and_return(true,false)
17
+ Product.stub!(:new).and_return(@active_record)
18
+ Product.stub!(:table_name).and_return('some_new_table')
19
+ Legacy::Product.stub!(:count).and_return(1)
20
+ Legacy::Product.stub!(:find).and_return([@legacy_record])
21
+ Legacy::Product.stub!(:table_name).and_return('some_old_table')
22
+ end
23
+
24
+ it "should find the legacy records" do
25
+ Legacy::Product.should_receive(:find).and_return([@legacy_record])
26
+ ProductOneMigration.new.run
27
+ end
28
+
29
+ it "should create a new active record" do
30
+ Product.should_receive(:new).and_return(@active_record)
31
+ ProductOneMigration.new.run
32
+ end
33
+
34
+ it "should get the legacy name value" do
35
+ @legacy_record.should_receive(:name).and_return("Beer")
36
+ ProductOneMigration.new.run
37
+ end
38
+
39
+ it "should set the active name value to the legacy value" do
40
+ @active_record.should_receive(:name=).with("Beer").and_return("Beer")
41
+ ProductOneMigration.new.run
42
+ end
43
+
44
+ it "should attempt to save the active record" do
45
+ @active_record.should_receive(:save).and_return(true)
46
+ ProductOneMigration.new.run
47
+ end
48
+
49
+ it "should call #handle_success" do
50
+ @migration = ProductOneMigration.new
51
+ @migration.should_receive(:handle_success)
52
+ @migration.run
53
+ end
54
+
55
+ describe "with specified find parameters (other than limit and offset)" do
56
+
57
+ it "should find the legacy records with the specified parameters" do
58
+ Legacy::Product.should_receive(:find).with(:all, {:conditions => ['name = ?', 'matt'], :include => :manufacturer}).and_return([@legacy_record])
59
+ ProductFiveMigration.new.run
60
+ end
61
+
62
+ end
63
+
64
+ describe "with the skip flag set before #migrate_field" do
65
+
66
+ before do
67
+ @migration = ProductSevenMigration.new
68
+ @active_record.stub!(:valid?).and_return(false)
69
+ @active_record.stub!(:errors).and_return([])
70
+ end
71
+
72
+ it "should not receive #save" do
73
+ @active_record.should_not_receive(:save)
74
+ @migration.run
75
+ end
76
+
77
+ it "should receive #handle_success" do
78
+ @migration.should_receive(:handle_success)
79
+ @migration.run
80
+ end
81
+
82
+ end
83
+
84
+ describe "with the validation flag set to false" do
85
+
86
+ before do
87
+ @migration = ProductNineMigration.new
88
+ @active_record.stub!(:valid?).and_return(false)
89
+ @active_record.stub!(:errors).and_return([])
90
+ end
91
+
92
+ it "should recieve #save with false" do
93
+ @active_record.should_receive(:save).with(false).and_return(true)
94
+ @migration.run
95
+ end
96
+
97
+ it "should receive #handle_success" do
98
+ @migration.should_receive(:handle_success)
99
+ @migration.run
100
+ end
101
+
102
+ end
103
+
104
+ describe "with the skip flag set in #handle_error" do
105
+
106
+ before do
107
+ @migration = ProductEightMigration.new
108
+ @active_record.stub!(:save).and_return(false)
109
+ @active_record.stub!(:valid?).and_return(false)
110
+ @active_record.stub!(:errors).and_return([])
111
+ end
112
+
113
+ it "should only call save once and then skip the migration" do
114
+ @active_record.should_receive(:save).once.and_return(false)
115
+ @migration.run
116
+ end
117
+
118
+ it "should receive #handle_success" do
119
+ @migration.should_receive(:handle_success)
120
+ @migration.run
121
+ end
122
+
123
+ end
124
+
125
+ describe "with specified limit and offset in find parameters" do
126
+
127
+ before do
128
+ @legacy_recordset = []
129
+ 10.times {@legacy_recordset.push(@legacy_record)}
130
+ Legacy::Product.stub!(:count).and_return(10)
131
+ Legacy::Product.stub!(:find).and_return(@legacy_recordset)
132
+ end
133
+
134
+ it "should not use the specified offset in the find parameters" do
135
+ Legacy::Product.should_not_receive(:find).with(:all, {:limit => 5, :offset => 3})
136
+ ProductOneMigration.new.run
137
+ end
138
+
139
+ it "should call find with a limit of max_rows(5) and an offset of 0 once" do
140
+ Legacy::Product.should_receive(:find).with(:all, {:limit => 5, :offset => 0}).once.and_return(@legacy_recordset)
141
+ ProductOneMigration.new.run
142
+ end
143
+
144
+ it "should call find with a limit of max_rows(5) and an offset of 5 once" do
145
+ Legacy::Product.should_receive(:find).with(:all, {:limit => 5, :offset => 5}).once.and_return(@legacy_recordset)
146
+ ProductOneMigration.new.run
147
+ end
148
+
149
+ end
150
+
151
+ describe "with the update flag set for the active model" do
152
+
153
+ before do
154
+ Product.stub!(:find).and_return(@active_record)
155
+ end
156
+
157
+ it "should call find on the active model" do
158
+ Product.should_receive(:find).with(1).and_return(@active_record)
159
+ ProductSixMigration.new.run
160
+ end
161
+
162
+ it "should not create a new active record" do
163
+ Product.should_not_receive(:new)
164
+ ProductSixMigration.new.run
165
+ end
166
+
167
+ end
168
+
169
+ describe "with invalid data in the active record" do
170
+
171
+ before do
172
+ @errors = mock('errors_object', :null_object => true)
173
+ @active_record.stub!(:name).and_return('Beer')
174
+ @active_record.stub!(:save).and_return(false)
175
+ @active_record.stub!(:save!).and_return(true)
176
+ @active_record.stub!(:valid?).and_return(false, true)
177
+ @active_record.stub!(:errors).and_return(@errors)
178
+ @migration = ProductOneMigration.new
179
+ @migration.stub!(:handle_error).and_return("new_value")
180
+ end
181
+
182
+ it "should attempt to save the active record" do
183
+ @active_record.should_receive(:save).and_return(false)
184
+ @migration.run
185
+ end
186
+
187
+ it "should call #handle_error" do
188
+ @migration.should_receive(:handle_error)
189
+ @migration.run
190
+ end
191
+
192
+ end
193
+
194
+ describe "with @active_record set as an Array" do
195
+
196
+ before do
197
+ @record1 = mock('active_model1', :id => 1, :name= => 'Beer', :save => true)
198
+ @record2 = mock('active_model2', :id => 2, :name= => 'Beer', :save => true)
199
+ @record1.stub!(:changed?).and_return(true,false)
200
+ @record2.stub!(:changed?).and_return(true,false)
201
+ @active_record = [@record1, @record2]
202
+ Product.stub!(:new).and_return(@active_record)
203
+ end
204
+
205
+ it "should call save on record1" do
206
+ @record1.should_receive(:save).and_return(true)
207
+ ProductOneMigration.new.run
208
+ end
209
+
210
+ it "should call save on record2" do
211
+ @record2.should_receive(:save).and_return(true)
212
+ ProductOneMigration.new.run
213
+ end
214
+
215
+ it "should receive handle_success for each record" do
216
+ @migration = ProductOneMigration.new
217
+ @migration.should_receive(:handle_success).twice
218
+ @migration.run
219
+ end
220
+
221
+ end
222
+
223
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ require File.dirname(__FILE__) + '/fixtures/product_one_migration'
4
+ require File.dirname(__FILE__) + '/fixtures/product_ten_migration'
5
+
6
+ describe "A migration" do
7
+
8
+ before do
9
+ ActiveMigration::Base.logger = mock('logger', :null_object => true)
10
+ @legacy_record = mock('legacy_model', :id => 1, :name => 'Beer')
11
+ @active_record = mock('active_model', :id => 1, :name= => 'Beer', :save => true)
12
+ @active_record.stub!(:changed?).and_return(true,false)
13
+ Product.stub!(:new).and_return(@active_record)
14
+ Product.stub!(:table_name).and_return('some_new_table')
15
+ Legacy::Product.stub!(:count).and_return(1)
16
+ Legacy::Product.stub!(:find).and_return([@legacy_record])
17
+ Legacy::Product.stub!(:table_name).and_return('some_old_table')
18
+ @migration = ProductOneMigration.new
19
+ end
20
+
21
+ describe "when active_record_mode is set to :create" do
22
+
23
+ create_callbacks = ActiveMigration::Callbacks::CALLBACKS - %w(before_update after_update)
24
+
25
+ create_callbacks.each do |callback|
26
+ it "should call ##{callback}" do
27
+ @migration.should_receive(callback).once
28
+ @migration.run
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ describe "when active_record_mode is set to :update" do
35
+
36
+ before do
37
+ @migration = ProductTenMigration.new
38
+ Product.stub!(:find).and_return(@active_record)
39
+ end
40
+
41
+ update_callbacks = ActiveMigration::Callbacks::CALLBACKS - %w(before_create after_create)
42
+
43
+ update_callbacks.each do |callback|
44
+ it "should call ##{callback}" do
45
+ @migration.should_receive(callback).once
46
+ @migration.run
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ require File.dirname(__FILE__) + '/fixtures/product_two_migration'
4
+
5
+ ManufacturerMigration = Class.new
6
+
7
+ describe "A migration" do
8
+
9
+ before do
10
+ ActiveMigration::Base.logger = mock('logger', :null_object => true)
11
+ @dependent_record = mock("dependency_model", :run => nil)
12
+ @legacy_record = mock('legacy_model', :id => 1, :name => 'Beer')
13
+ @active_record = mock('active_model', :id => 1, :name= => 'Beer', :save => true)
14
+ @active_record.stub!(:changed?).and_return(true,false)
15
+ Product.stub!(:new).and_return(@active_record)
16
+ Product.stub!(:table_name).and_return('some_new_table')
17
+ Legacy::Product.stub!(:count).and_return(1)
18
+ Legacy::Product.stub!(:find).and_return([@legacy_record])
19
+ Legacy::Product.stub!(:table_name).and_return('some_old_table')
20
+ ManufacturerMigration.stub!(:new).and_return(@dependent_record)
21
+ ManufacturerMigration.stub!(:run).and_return(nil)
22
+ ManufacturerMigration.stub!(:completed?).and_return(false)
23
+ ManufacturerMigration.stub!(:is_completed).and_return(true)
24
+ end
25
+
26
+ it "should instansiate it's dependencies" do
27
+ ManufacturerMigration.should_receive(:new).and_return(@dependent_record)
28
+ ProductTwoMigration.new.run
29
+ end
30
+
31
+ it "should run it's dependencies" do
32
+ @dependent_record.should_receive(:run).and_return(nil)
33
+ ProductTwoMigration.new.run
34
+ end
35
+
36
+ end
@@ -0,0 +1,13 @@
1
+ class ProductEightMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product'
6
+
7
+ map [['name', 'name']]
8
+
9
+ def handle_error
10
+ @skip = true
11
+ end
12
+
13
+ end
@@ -0,0 +1,11 @@
1
+ class ProductFiveMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product',
6
+ :conditions => ['name = ?', 'matt'],
7
+ :include => :manufacturer
8
+
9
+ map [['name', 'name']]
10
+
11
+ end
@@ -0,0 +1,9 @@
1
+ class ProductFourMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product'
6
+
7
+ map [['supplier_id' , 'supplier_id', :products]]
8
+
9
+ end
@@ -0,0 +1,13 @@
1
+ class ProductNineMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product'
6
+
7
+ map [['name', 'name']]
8
+
9
+ def before_save
10
+ @validate_record = false
11
+ end
12
+
13
+ end
@@ -0,0 +1,11 @@
1
+ class ProductOneMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product',
6
+ :limit => 5,
7
+ :offset => 3 # This is for specing only, this element will be deleted.
8
+
9
+ map [['name', 'name']]
10
+
11
+ end
@@ -0,0 +1,13 @@
1
+ class ProductSevenMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product'
6
+
7
+ map [['name', 'name']]
8
+
9
+ def before_migrate_field
10
+ @skip = true
11
+ end
12
+
13
+ end
@@ -0,0 +1,10 @@
1
+ class ProductSixMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product',
4
+ :update
5
+
6
+ set_legacy_model 'Legacy::Product'
7
+
8
+ map [['name', 'name']]
9
+
10
+ end
@@ -0,0 +1,12 @@
1
+ class ProductTenMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product',
4
+ :mode => :update
5
+
6
+ set_legacy_model 'Legacy::Product',
7
+ :limit => 5,
8
+ :offset => 3 # This is for specing only, this element will be deleted.
9
+
10
+ map [['name', 'name']]
11
+
12
+ end
@@ -0,0 +1,11 @@
1
+ class ProductThreeMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product'
6
+
7
+ map [['name', 'name']]
8
+
9
+ write_key_map true
10
+
11
+ end
@@ -0,0 +1,11 @@
1
+ class ProductTwoMigration < ActiveMigration::Base
2
+
3
+ set_active_model 'Product'
4
+
5
+ set_legacy_model 'Legacy::Product'
6
+
7
+ map [['name', 'name']]
8
+
9
+ set_dependencies [:manufacturer_migration]
10
+
11
+ end