migratrix 0.0.9 → 0.8.1

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.
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