migratrix 0.0.9 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/lib/migratrix.rb +62 -6
  2. data/lib/migratrix/exceptions.rb +4 -1
  3. data/lib/migratrix/{extractors → extractions}/active_record.rb +14 -10
  4. data/lib/migratrix/{extractors/extractor.rb → extractions/extraction.rb} +21 -20
  5. data/lib/migratrix/loads/load.rb +43 -0
  6. data/lib/migratrix/loads/yaml.rb +15 -0
  7. data/lib/migratrix/migration.rb +115 -27
  8. data/lib/migratrix/migratrix.rb +43 -84
  9. data/lib/migratrix/registry.rb +20 -0
  10. data/lib/migratrix/transforms/map.rb +57 -0
  11. data/lib/migratrix/transforms/transform.rb +268 -0
  12. data/lib/migratrix/valid_options.rb +22 -0
  13. data/lib/patches/object_ext.rb +0 -4
  14. data/spec/fixtures/migrations/marbles_migration.rb +6 -4
  15. data/spec/lib/migratrix/{loggable_spec.rb → _loggable_spec.rb} +0 -0
  16. data/spec/lib/migratrix/extractions/active_record_spec.rb +146 -0
  17. data/spec/lib/migratrix/extractions/extraction_spec.rb +71 -0
  18. data/spec/lib/migratrix/loads/load_spec.rb +59 -0
  19. data/spec/lib/migratrix/loads/yaml_spec.rb +39 -0
  20. data/spec/lib/migratrix/migration_spec.rb +195 -27
  21. data/spec/lib/migratrix/migratrix_spec.rb +57 -85
  22. data/spec/lib/migratrix/registry_spec.rb +28 -0
  23. data/spec/lib/migratrix/transforms/map_spec.rb +55 -0
  24. data/spec/lib/migratrix/transforms/transform_spec.rb +134 -0
  25. data/spec/lib/migratrix_spec.rb +98 -0
  26. data/spec/lib/patches/object_ext_spec.rb +0 -7
  27. data/spec/spec_helper.rb +18 -13
  28. metadata +21 -10
  29. data/spec/lib/migratrix/extractors/active_record_spec.rb +0 -43
  30. data/spec/lib/migratrix/extractors/extractor_spec.rb +0 -63
  31. data/spec/lib/migratrix_module_spec.rb +0 -63
@@ -1,79 +1,12 @@
1
1
  # Main "App" or Driver class for Migrating. Responsible for loading
2
2
  # and integrating all the parts of a migration.
3
3
  module Migratrix
4
- include ::Migratrix::Loggable
5
-
6
- def self.migrate!(name, options={})
7
- ::Migratrix::Migratrix.migrate(name, options)
8
- end
9
-
10
- def self.reload_migration(name)
11
- ::Migratrix::Migratrix.reload_migration(name)
12
- end
13
-
14
- def self.logger
15
- ::Migratrix::Migratrix.logger
16
- end
17
-
18
- def self.logger=(new_logger)
19
- ::Migratrix::Migratrix.logger = new_logger
20
- end
21
-
22
- def self.log_to(stream)
23
- ::Migratrix::Migratrix.log_to(stream)
24
- end
25
-
26
4
  class Migratrix
27
5
  include ::Migratrix::Loggable
28
6
 
29
7
  def initialize
30
8
  end
31
9
 
32
- def self.migrate(name, options={})
33
- migratrix = self.new()
34
- migration = migratrix.create_migration(name, options)
35
- migration.migrate
36
- migratrix
37
- end
38
-
39
- # Loads #{name}_migration.rb from migrations path, instantiates
40
- # #{Name}Migration with options, and returns it.
41
- def create_migration(name, options={})
42
- options = filter_options(options)
43
- klass_name = migration_name(name)
44
- unless loaded?(klass_name)
45
- raise MigrationAlreadyExists.new("Migratrix cannot instantiate class Migratrix::#{klass_name} because it already exists") if ::Migratrix.const_defined?(klass_name)
46
- reload_migration name
47
- raise MigrationNotDefined.new("Expected migration file #{filename} to define Migratrix::#{klass_name} but it did not") unless ::Migratrix.const_defined?(klass_name)
48
- register_migration(klass_name, "Migratrix::#{klass_name}".constantize)
49
- end
50
- fetch_migration(klass_name).new(options)
51
- end
52
-
53
- def migration_name(name)
54
- name = name.to_s
55
- name = if name.plural?
56
- name.classify.pluralize
57
- else
58
- name.classify
59
- end
60
- name + "Migration"
61
- end
62
-
63
- def filter_options(hash)
64
- Hash[valid_options.map {|v| hash.key?(v) ? [v, hash[v]] : nil }.compact]
65
- end
66
-
67
- def valid_options
68
- %w(limit offset order where)
69
- end
70
-
71
- def reload_migration(name)
72
- filename = migrations_path + "#{name}_migration.rb"
73
- raise MigrationFileNotFound.new("Migratrix cannot find migration file #{filename}") unless File.exists?(filename)
74
- load filename
75
- end
76
-
77
10
  # ----------------------------------------------------------------------
78
11
  # Logger singleton; tries to hook into Rails.logger if it exists (it
79
12
  # won't if you log anything during startup because Migratrix is
@@ -93,15 +26,15 @@ module Migratrix
93
26
 
94
27
  def self.init_logger
95
28
  return Rails.logger if Rails.logger
96
- @@logger = create_logger($stdout)
29
+ @logger = create_logger($stdout)
97
30
  end
98
31
 
99
32
  def self.logger
100
- @@logger ||= self.init_logger
33
+ @logger ||= self.init_logger
101
34
  end
102
35
 
103
36
  def self.logger=(new_logger)
104
- @@logger = new_logger
37
+ @logger = new_logger
105
38
  end
106
39
  # ----------------------------------------------------------------------
107
40
 
@@ -109,30 +42,56 @@ module Migratrix
109
42
 
110
43
  # ----------------------------------------------------------------------
111
44
  # Candidate for exract class? MigrationRegistry?
112
- def loaded?(name)
113
- registered_migrations.key? name.to_s
45
+ def self.registry
46
+ @registry ||= Hash[[:extractions,:loads,:migrations,:transforms].map {|key| [key, Registry.new]}]
114
47
  end
115
48
 
116
- def register_migration(name, klass)
117
- registered_migrations[name.to_s] = klass
49
+ # --------------------
50
+ # extractions
51
+ def self.extractions
52
+ registry[:extractions]
118
53
  end
119
54
 
120
- def fetch_migration(name)
121
- registered_migrations.fetch name.to_s
55
+ def self.register_extraction(class_name, klass, options={})
56
+ self.extractions.register(class_name, klass, options)
122
57
  end
123
58
 
124
- def registered_migrations
125
- @@registered_migrations ||= {}
59
+ def self.extraction(class_name, extraction_name, options={})
60
+ self.extractions.class_for(class_name).new(extraction_name, options)
61
+ end
62
+ # --------------------
63
+
64
+ # --------------------
65
+ # transforms
66
+ def self.transforms
67
+ registry[:transforms]
126
68
  end
127
- # End MigrationRegistry
128
- # ----------------------------------------------------------------------
129
69
 
130
- def migrations_path
131
- @migrations_path ||= ::Migratrix.default_migrations_path
70
+ def self.register_transform(name, klass, options={})
71
+ self.transforms.register(name, klass, options)
132
72
  end
133
73
 
134
- def migrations_path=(new_path)
135
- @migrations_path = Pathname.new new_path
74
+ def self.transform(transform_name, class_name, options={})
75
+ self.transforms.class_for(class_name).new(transform_name, options)
136
76
  end
77
+ # --------------------
78
+
79
+ # --------------------
80
+ # loads
81
+ def self.loads
82
+ registry[:loads]
83
+ end
84
+
85
+ def self.register_load(name, klass, options={})
86
+ self.loads.register(name, klass, options)
87
+ end
88
+
89
+ def self.load(load_name, class_name, options={})
90
+ self.loads.class_for(class_name).new(load_name, options)
91
+ end
92
+ # --------------------
93
+
94
+ # End MigrationRegistry
95
+ # ----------------------------------------------------------------------
137
96
  end
138
97
  end
@@ -0,0 +1,20 @@
1
+ module Migratrix
2
+ # Basically a place to store our factories
3
+ class Registry
4
+ def register(name, klass, init_options)
5
+ registry[name] = [klass, init_options]
6
+ end
7
+
8
+ def class_for(name)
9
+ registry.fetch(name).first
10
+ end
11
+
12
+ def registered?(name)
13
+ registry.key?(name)
14
+ end
15
+
16
+ def registry
17
+ @registry ||= {}
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,57 @@
1
+ module Migratrix
2
+ module Transforms
3
+ # Map is a transform that maps attributes from the source object
4
+ # to the target object.
5
+ #
6
+ # :transform: a hash with dst => src keys, where dst is an
7
+ # attribute on the transformed target object and src is either an
8
+ # attribute on the source object or a Proc that receives the
9
+ # entire extracted row and returns a value to be set.
10
+ #
11
+ # TODO: Right now map makes a lot of hard-coded assumptions as a
12
+ # result of the primary test case. Notably that target is a Hash,
13
+ # final class is a Hash keyed by transformed_object[:id], etc.
14
+ #
15
+ # TODO: Figure out how to do both of these strategies with Map:
16
+ #
17
+ # # Create object and then modify it sequentially
18
+ # new_object = target.new
19
+ # map.each do |dst, src|
20
+ # new_object[dst] = extracted_item[src]
21
+ # end
22
+ #
23
+ # # Build up creation params and then new the object
24
+ # hash = Hash.new
25
+ # map.each do [dst, src]
26
+ # hash[dst] = extracted_item[src]
27
+ # end
28
+ # new_object = target.new(hash)
29
+ class Map < Transform
30
+ attr_accessor :map
31
+
32
+ def initialize(name, options={})
33
+ super
34
+ end
35
+
36
+ def create_transformed_collection
37
+ Hash.new
38
+ end
39
+
40
+ def create_new_object(extracted_row)
41
+ Hash.new
42
+ end
43
+
44
+ def apply_attribute(object, attribute_or_apply, value)
45
+ object[attribute_or_apply] = value
46
+ end
47
+
48
+ def extract_attribute(object, attribute_or_extract)
49
+ object[attribute_or_extract]
50
+ end
51
+
52
+ def store_transformed_object(object, collection)
53
+ collection[object[:id]] = object
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,268 @@
1
+ module Migratrix
2
+ module Transforms
3
+ # Transform base class. A transform takes a collection of
4
+ # extracted objects and returns a collection of transformed
5
+ # objects. To do this, it needs to know the following:
6
+ #
7
+ # 1. What kind of collection to create. (Array? Hash? Set?
8
+ # Custom?)
9
+ # 2. How to transform one extracted object into a transformed
10
+ # object.
11
+ # 2.1 How to create the object that will hold the transformed
12
+ # object's attributes (might be the transformed object's class,
13
+ # might be a hash of attributes used to create the object itself,
14
+ # you might even try loading the target object from the
15
+ # destination db to see if it's already been migrated--and if so,
16
+ # can it be skipped or does it need to be updated?)
17
+ # 2.2 What attributes to pull off the extracted object
18
+ # 2.3 HOW to pull an attribute off the extracted object
19
+ # 2.4 How to transform each attribute
20
+ # 2.5 What attributes to store on the target object
21
+ # 2.6 HOW to store an attribute on the target object
22
+ # 2.7 How to finalize the transformation
23
+ # 2.7.1 Did you create the target object by class and have you
24
+ # been updating it all along? Then yay, this step is a no-op and
25
+ # you're already done.
26
+ # 2.7.2 If you're doing a merge transformation, and you haven't
27
+ # already checked the destination db for an existing record, now's
28
+ # the time to try to load that sucker and, if successful, to merge
29
+ # it with your migrated values.
30
+ # 2.7.3 A common Rails optimization is to create an attributes
31
+ # hash and new the ActiveRecord object in one call. This is
32
+ # faster* than creating a blank object and updating its attributes
33
+ # individually.
34
+ # 2.8 How to store the finalized object in the collection.
35
+ # (hash[id]=obj? set << obj?)
36
+ #
37
+ # And remember, all of this is just to handle ONE record, albeit
38
+ # admittedly in the most complicated way possible . For sql->sql
39
+ # migrations the transformation step is pretty simple. (It doesn't
40
+ # exist, ha ha. A cross-database INSERT SELECT means that the
41
+ # extract, transform and load all take place in the extract step.)
42
+ #
43
+ # * DANGER DANGER DANGER This is a completely unsubstantiated
44
+ # optimization claim. I'm *sure* it's faster, though, so that's
45
+ # good enough, right? TODO: BENCHMARK THIS OR SOMETHING WHATEVER
46
+ class Transform
47
+ include ::Migratrix::Loggable
48
+ include ::Migratrix::ValidOptions
49
+ attr_accessor :name, :options, :extraction, :transformations
50
+
51
+ set_valid_options(
52
+ :apply_attribute,
53
+ :extract_attribute,
54
+ :extraction,
55
+ :final_class,
56
+ :finalize_object,
57
+ :store_transform,
58
+ :target,
59
+ :transform,
60
+ :transform_class,
61
+ :transform_collection
62
+ )
63
+
64
+ def initialize(name, options={})
65
+ @name = name
66
+ @options = options.symbolize_keys
67
+ @transformations = options[:transform]
68
+ end
69
+
70
+ # Name of the extraction to use. If omitted, returns our name.
71
+ def extraction
72
+ options[:extraction] || name
73
+ end
74
+
75
+ # This transform method has strategy magic at every turn. I
76
+ # expect it to be slow, but we can optimize it later, e.g. by
77
+ # rolling out a define_method or similar for all of the constant
78
+ # parts.
79
+ #
80
+ # ----------------------------------------------------------------------
81
+ # Map's strategy, as used by PetsMigration
82
+ #
83
+ # create_transformed_collection -> Hash.new
84
+ # create_new_object -> Hash.new
85
+ # transformation -> {:id => :id, :name => :name }
86
+ # extract_attribute -> object[attribute_or_extract]
87
+ # apply_attribute -> object[attribute] = attribute_or_apply
88
+ # finalize_object -> no-op
89
+ # store_transformed_object -> collection[object[:id]] = object
90
+ # ----------------------------------------------------------------------
91
+ #
92
+ # ----------------------------------------------------------------------
93
+ # Default strategy:
94
+ #
95
+ # create_transformed_collection -> Array.new
96
+ # create_new_object -> @options[:target].new
97
+ # transformations -> MUST BE SUPPLIED BY CHILD CLASS, e.g. Map's {:dst => :src} hash
98
+ # extract_attribute -> attribute_or_extract.is_a?(Proc) ? attribute_or_extract.call(object) : object.send(attribute_or_extract)
99
+ # apply_attribute -> object.send("#{attribute_or_apply}=", value)
100
+ # finalize_object -> no-op
101
+ # store_transformed_object -> collection << transformed_object
102
+ # ----------------------------------------------------------------------
103
+ #
104
+ #
105
+ # Now, can we represent these two strategies as configurations
106
+ # in the migration? For example, here's one way of representing
107
+ # PetTypeMigration's strategy, which in the Load step must save
108
+ # off a YAML dump of a hash of all the pet objects (with just id
109
+ # and name) keyed
110
+ #
111
+ # set_transform :repetition_types, :map,
112
+ # :map => {:id => :id, :name => :name },
113
+ # :extract_method => :index,
114
+ # :apply_method => :index,
115
+ # :new_class => Hash,
116
+ # :collection => Hash,
117
+ # :store_method => lambda {|item, hash| hash[item[:id]] = item },
118
+ #
119
+ # So the backing magic is this:
120
+ #
121
+ # - We expect :new_class to respond to new() and give us a new,
122
+ # blank object.
123
+ #
124
+ # - Ditto for :collection; it should respond to new()
125
+ #
126
+ # - extract_method :index means attr = extracted_object[:name]
127
+ #
128
+ # - apply_method :index means the same thing
129
+ #
130
+ # - store_method is the only weirdness here, and it might
131
+ # actually make sense to have names for the most obvious
132
+ # strategies, like :store_by => { :index => :id } (An array
133
+ # could use :store_by => :push, Set by :add, etc.)
134
+ #
135
+ # - The :final_class option is optional; its presence interacts
136
+ # with the also-optional :finalize option.
137
+ #
138
+ # - The :finalize option is optional; its presence interacts
139
+ # with the also-optional :final_class option, as follows:
140
+ #
141
+ # Both Missing: final_object = new_object
142
+ # :final_class only: final_obj = FinalClass.new(new_obj)
143
+ # :finalize only: final_obj = finalize(new_obj)
144
+ # Both Present: final_obj = FinalClass.new(finalize(obj))
145
+ #
146
+ # Examples/Notes:
147
+ #
148
+ # - To build ActiveRecord objects quickly, use :new_class =>
149
+ # Hash, :final_class => ModelClass, :finalize => nil.
150
+ #
151
+ # - If your source row is a hash (e.g. select_all or a YAML
152
+ # load) that needs no transformation, but has more columns
153
+ # than your ActiveRecord model can accept, you could use a
154
+ # copy transform (basically a map transform without the map
155
+ # step; new_object = extracted_object) with something like
156
+ # :new_class => Hash, :final_class => ModelClass, :finalize
157
+ # => lambda {|hsh| hsh.dup.keep_if? {|k,v|
158
+ # k.in?(ModelClass.new.attributes.keys)}}
159
+ # (That would be HORRIBLY inefficient but there would be
160
+ # ways to memoize with e.g. before_finalize { @keys =
161
+ # ModelClass.new.attributes.keys })
162
+ #
163
+ # Advanced Magic:
164
+ # - new_class, collection can be procs returning a new object
165
+ # - extract_method, apply_method can be procs taking |obj, attr|
166
+ # and |obj, attr, value|
167
+ #
168
+ # Super Advanced Magic (YAGNI):
169
+ # - instead of procs, take blocks and use define_method on them
170
+ # so they're faster.
171
+ def transform(extracted_objects)
172
+ info "Transform #{name} started transform."
173
+ transformed_collection = create_transformed_collection
174
+ extracted_objects.each do |extracted_object|
175
+ new_object = create_new_object(extracted_object)
176
+ transformations.each do |attribute_or_apply, attribute_or_extract|
177
+ apply_attribute(new_object, attribute_or_apply, extract_attribute(extracted_object, attribute_or_extract))
178
+ end
179
+ transformed_object = finalize_object(new_object)
180
+ store_transformed_object(transformed_object, transformed_collection)
181
+ end
182
+ info "Transform #{name} finished transform."
183
+ transformed_collection
184
+ end
185
+
186
+
187
+ def create_transformed_collection
188
+ raise NotImplementedError unless options[:transform_collection]
189
+ option = options[:transform_collection]
190
+ case option
191
+ when Proc
192
+ option.call
193
+ when Class
194
+ option.new
195
+ else
196
+ raise TypeError
197
+ end
198
+ end
199
+
200
+ def create_new_object(extracted_row)
201
+ # TODO: this should work like finalize, taking a create and an init.
202
+ raise NotImplementedError unless options[:transform_class]
203
+ option = options[:transform_class]
204
+ case option
205
+ when Proc
206
+ option.call(extracted_row)
207
+ when Class
208
+ option.new # laaame--should receive extracted_row, see todo above
209
+ else
210
+ raise TypeError
211
+ end
212
+ end
213
+
214
+ def apply_attribute(object, attribute_or_apply, value)
215
+ raise NotImplementedError unless options[:apply_attribute]
216
+ option = options[:apply_attribute]
217
+ case option
218
+ when Proc
219
+ option.call(object, attribute_or_apply, value)
220
+ when Symbol
221
+ object.send(option, attribute_or_apply, value)
222
+ else
223
+ raise TypeError
224
+ end
225
+ end
226
+
227
+ def extract_attribute(object, attribute_or_extract)
228
+ raise NotImplementedError unless options[:extract_attribute]
229
+ option = options[:extract_attribute]
230
+ case option
231
+ when Proc
232
+ option.call(object, attribute_or_extract)
233
+ when Symbol
234
+ object.send(option, attribute_or_extract)
235
+ else
236
+ raise TypeError
237
+ end
238
+ end
239
+
240
+
241
+ # Both Missing: final_object = new_object
242
+ # :final_class only: final_obj = FinalClass.new(new_obj)
243
+ # :finalize only: final_obj = finalize(new_obj)
244
+ # Both Present: final_obj = FinalClass.new(finalize(obj))
245
+ def finalize_object(new_object)
246
+ return new_object unless options[:final_class] || options[:finalize_object]
247
+ raise TypeError if options[:finalize_object] && !options[:finalize_object].is_a?(Proc)
248
+ raise TypeError if options[:final_class] && !options[:final_class].is_a?(Class)
249
+ new_object = options[:finalize_object].call(new_object) if options[:finalize_object]
250
+ new_object = options[:final_class].new(new_object) if options[:final_class]
251
+ new_object
252
+ end
253
+
254
+ def store_transformed_object(object, collection)
255
+ raise NotImplementedError unless options[:store_transformed_object]
256
+ option = options[:store_transformed_object]
257
+ case option
258
+ when Proc
259
+ option.call(object, collection)
260
+ when Symbol
261
+ collection.send(option, object)
262
+ else
263
+ raise TypeError
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end