replicate 1.5 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -137,6 +137,12 @@ class User < ActiveRecord::Base
137
137
  end
138
138
  ```
139
139
 
140
+ You may also do this by passing an option in your dump script:
141
+
142
+ ```ruby
143
+ dump User.all, :associations => [:email_addresses]
144
+ ```
145
+
140
146
  ### Natural Keys
141
147
 
142
148
  By default, the loader attempts to create a new record with a new primary key id
@@ -178,6 +184,12 @@ end
178
184
 
179
185
  You can omit belongs_to associations by omitting the foreign key column.
180
186
 
187
+ You may also do this by passing an option in your dump script:
188
+
189
+ ```ruby
190
+ dump User.all, :omit => [:profile]
191
+ ```
192
+
181
193
  ### Validations and Callbacks
182
194
 
183
195
  __IMPORTANT:__ All ActiveRecord validations and callbacks are disabled on the
@@ -211,7 +223,7 @@ the `dump_replicant` and `load_replicant` methods.
211
223
 
212
224
  ### dump_replicant
213
225
 
214
- The dump side calls `#dump_replicant(dumper)` on each object. The method must
226
+ The dump side calls `#dump_replicant(dumper, opts={})` on each object. The method must
215
227
  call `dumper.write()` with the class name, id, and hash of primitively typed
216
228
  attributes for the object:
217
229
 
@@ -220,7 +232,7 @@ class User
220
232
  attr_reader :id
221
233
  attr_accessor :name, :email
222
234
 
223
- def dump_replicant(dumper)
235
+ def dump_replicant(dumper, opts={})
224
236
  attributes = { 'name' => name, 'email' => email }
225
237
  dumper.write self.class, id, attributes, self
226
238
  end
@@ -19,15 +19,28 @@ module Replicate
19
19
  # type, id, and attributes hash.
20
20
  #
21
21
  # Returns nothing.
22
- def dump_replicant(dumper)
22
+ def dump_replicant(dumper, opts={})
23
+ @replicate_opts = opts
24
+ @replicate_opts[:associations] ||= []
25
+ @replicate_opts[:omit] ||= []
23
26
  dump_all_association_replicants dumper, :belongs_to
24
27
  dumper.write self.class.to_s, id, replicant_attributes, self
25
28
  dump_all_association_replicants dumper, :has_one
26
- self.class.replicate_associations.each do |association|
29
+ included_associations.each do |association|
27
30
  dump_association_replicants dumper, association
28
31
  end
29
32
  end
30
33
 
34
+ # List of associations to explicitly include when dumping this object.
35
+ def included_associations
36
+ (self.class.replicate_associations + @replicate_opts[:associations]).uniq
37
+ end
38
+
39
+ # List of attributes and associations to omit when dumping this object.
40
+ def omitted_attributes
41
+ (self.class.replicate_omit_attributes + @replicate_opts[:omit]).uniq
42
+ end
43
+
31
44
  # Attributes hash used to persist this object. This consists of simply
32
45
  # typed values (no complex types or objects) with the exception of special
33
46
  # foreign key values. When an attribute value is [:id, "SomeClass:1234"],
@@ -35,39 +48,68 @@ module Replicate
35
48
  # version of the same object.
36
49
  def replicant_attributes
37
50
  attributes = self.attributes.dup
38
- self.class.replicate_omit_attributes.each do |omit|
39
- attributes.delete(omit.to_s)
40
- end
51
+
52
+ omitted_attributes.each { |omit| attributes.delete(omit.to_s) }
41
53
  self.class.reflect_on_all_associations(:belongs_to).each do |reflection|
42
- options = reflection.options
43
- if options[:polymorphic]
44
- if ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR > 0
45
- reference_class = attributes[reflection.foreign_type]
46
- else
47
- reference_class = attributes[options[:foreign_type]]
54
+ if info = replicate_reflection_info(reflection)
55
+ if replicant_id = info[:replicant_id]
56
+ foreign_key = info[:foreign_key].to_s
57
+ attributes[foreign_key] = [:id, *replicant_id]
48
58
  end
49
- next if reference_class.nil?
50
-
51
- klass = Kernel.const_get(reference_class)
52
- primary_key = klass.primary_key
53
- foreign_key = "#{reflection.name}_id"
54
- else
55
- klass = reflection.klass
56
- primary_key = (options[:primary_key] || klass.primary_key).to_s
57
- foreign_key = (options[:foreign_key] || "#{reflection.name}_id").to_s
58
59
  end
59
- if primary_key == klass.primary_key
60
- if id = attributes[foreign_key]
61
- attributes[foreign_key] = [:id, klass.to_s, id]
60
+ end
61
+
62
+ attributes
63
+ end
64
+
65
+ # Retrieve information on a reflection's associated class and various
66
+ # keys.
67
+ #
68
+ # Returns an info hash with these keys:
69
+ # :class - The class object the association points to.
70
+ # :primary_key - The string primary key column name.
71
+ # :foreign_key - The string foreign key column name.
72
+ # :replicant_id - The [classname, id] tuple identifying the record.
73
+ #
74
+ # Returns nil when the reflection can not be linked to a model.
75
+ def replicate_reflection_info(reflection)
76
+ options = reflection.options
77
+ if options[:polymorphic]
78
+ reference_class =
79
+ if ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR > 0
80
+ attributes[reflection.foreign_type]
62
81
  else
63
- # nil value in association reference
82
+ attributes[options[:foreign_type]]
64
83
  end
84
+ return if reference_class.nil?
85
+
86
+ klass = Kernel.const_get(reference_class)
87
+ primary_key = klass.primary_key
88
+ foreign_key = "#{reflection.name}_id"
89
+ else
90
+ klass = reflection.klass
91
+ primary_key = (options[:primary_key] || klass.primary_key).to_s
92
+ foreign_key = (options[:foreign_key] || "#{reflection.name}_id").to_s
93
+ end
94
+
95
+ info = {
96
+ :class => klass,
97
+ :primary_key => primary_key,
98
+ :foreign_key => foreign_key
99
+ }
100
+
101
+ if primary_key == klass.primary_key
102
+ if id = attributes[foreign_key]
103
+ info[:replicant_id] = [klass.to_s, id]
65
104
  else
66
- # association uses non-primary-key foreign key. no special key
67
- # conversion needed.
105
+ # nil value in association reference
68
106
  end
107
+ else
108
+ # association uses non-primary-key foreign key. no special key
109
+ # conversion needed.
69
110
  end
70
- attributes
111
+
112
+ info
71
113
  end
72
114
 
73
115
  # The replicant id is a two tuple containing the class and object id. This
@@ -85,11 +127,25 @@ module Replicate
85
127
  # Returns nothing.
86
128
  def dump_all_association_replicants(dumper, association_type)
87
129
  self.class.reflect_on_all_associations(association_type).each do |reflection|
88
- next if self.class.replicate_omit_attributes.include?(reflection.name)
130
+ next if omitted_attributes.include?(reflection.name)
131
+
132
+ # bail when this object has already been dumped
133
+ next if (info = replicate_reflection_info(reflection)) &&
134
+ (replicant_id = info[:replicant_id]) &&
135
+ dumper.dumped?(replicant_id)
136
+
89
137
  next if (dependent = __send__(reflection.name)).nil?
138
+
90
139
  case dependent
91
140
  when ActiveRecord::Base, Array
92
141
  dumper.dump(dependent)
142
+
143
+ # clear reference to allow GC
144
+ if respond_to?(:association)
145
+ association(reflection.name).reset
146
+ elsif respond_to?(:association_instance_set, true)
147
+ association_instance_set(reflection.name, nil)
148
+ end
93
149
  else
94
150
  warn "warn: #{self.class}##{reflection.name} #{association_type} association " \
95
151
  "unexpectedly returned a #{dependent.class}. skipping."
@@ -110,6 +166,7 @@ module Replicate
110
166
  if reflection.macro == :has_and_belongs_to_many
111
167
  dump_has_and_belongs_to_many_replicant(dumper, reflection)
112
168
  end
169
+ __send__(reflection.name).reset # clear to allow GC
113
170
  else
114
171
  warn "error: #{self.class}##{association} is invalid"
115
172
  end
@@ -284,7 +341,7 @@ module Replicate
284
341
  }
285
342
  end
286
343
 
287
- def dump_replicant(dumper)
344
+ def dump_replicant(dumper, opts={})
288
345
  type = self.class.name
289
346
  id = "#{@object.class.to_s}:#{@reflection.name}:#{@object.id}"
290
347
  dumper.write type, id, attributes, self
@@ -69,11 +69,18 @@ module Replicate
69
69
  #
70
70
  # Returns nothing.
71
71
  def dump(*objects)
72
+ opts = if objects.last.is_a? Hash
73
+ objects.pop
74
+ else
75
+ {}
76
+ end
72
77
  objects = objects[0] if objects.size == 1 && objects[0].respond_to?(:to_ary)
73
78
  objects.each do |object|
74
79
  next if object.nil? || dumped?(object)
75
80
  if object.respond_to?(:dump_replicant)
76
- object.dump_replicant(self)
81
+ args = [self]
82
+ args << opts unless object.method(:dump_replicant).arity == 1
83
+ object.dump_replicant(*args)
77
84
  else
78
85
  raise NoMethodError, "#{object.class} must respond to #dump_replicant"
79
86
  end
@@ -40,7 +40,7 @@ module Replicate
40
40
  value
41
41
  end
42
42
 
43
- def dump_replicant(dumper)
43
+ def dump_replicant(dumper, opts={})
44
44
  dumper.write self.class, @id, @attributes, self
45
45
  end
46
46
 
@@ -293,6 +293,59 @@ class ActiveRecordTest < Test::Unit::TestCase
293
293
  assert_equal rtomayko.emails.last, obj
294
294
  end
295
295
 
296
+ def test_dumping_associations_at_dump_time
297
+ objects = []
298
+ @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
299
+
300
+ rtomayko = User.find_by_login('rtomayko')
301
+ @dumper.dump rtomayko, :associations => [:emails], :omit => [:profile]
302
+
303
+ assert_equal 3, objects.size
304
+
305
+ type, id, attrs, obj = objects.shift
306
+ assert_equal 'User', type
307
+ assert_equal rtomayko.id, id
308
+ assert_equal 'rtomayko', attrs['login']
309
+ assert_equal rtomayko.created_at, attrs['created_at']
310
+ assert_equal rtomayko, obj
311
+
312
+ type, id, attrs, obj = objects.shift
313
+ assert_equal 'Email', type
314
+ assert_equal 'ryan@github.com', attrs['email']
315
+ assert_equal [:id, 'User', rtomayko.id], attrs['user_id']
316
+ assert_equal rtomayko.emails.first, obj
317
+
318
+ type, id, attrs, obj = objects.shift
319
+ assert_equal 'Email', type
320
+ assert_equal 'rtomayko@gmail.com', attrs['email']
321
+ assert_equal [:id, 'User', rtomayko.id], attrs['user_id']
322
+ assert_equal rtomayko.emails.last, obj
323
+ end
324
+
325
+ def test_dumping_many_associations_at_dump_time
326
+ objects = []
327
+ @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
328
+
329
+ users = User.all(:conditions => {:login => %w[rtomayko kneath]})
330
+ @dumper.dump users, :associations => [:emails], :omit => [:profile]
331
+
332
+ assert_equal 5, objects.size
333
+ assert_equal ['Email', 'Email', 'Email', 'User', 'User'], objects.map { |type,_,_| type }.sort
334
+ end
335
+
336
+ def test_omit_attributes_at_dump_time
337
+ objects = []
338
+ @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
339
+
340
+ rtomayko = User.find_by_login('rtomayko')
341
+ @dumper.dump rtomayko, :omit => [:created_at]
342
+
343
+ type, id, attrs, obj = objects.shift
344
+ assert_equal 'User', type
345
+ assert attrs['updated_at']
346
+ assert_nil attrs['created_at']
347
+ end
348
+
296
349
  def test_dumping_polymorphic_associations
297
350
  objects = []
298
351
  @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
metadata CHANGED
@@ -1,60 +1,55 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: replicate
3
- version: !ruby/object:Gem::Version
4
- hash: 5
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.1
5
5
  prerelease:
6
- segments:
7
- - 1
8
- - 5
9
- version: "1.5"
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Ryan Tomayko
13
9
  autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
-
17
- date: 2011-10-19 00:00:00 -07:00
18
- default_executable:
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
12
+ date: 2011-10-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
21
15
  name: activerecord
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
24
17
  none: false
25
- requirements:
18
+ requirements:
26
19
  - - ~>
27
- - !ruby/object:Gem::Version
28
- hash: 5
29
- segments:
30
- - 3
31
- - 1
32
- version: "3.1"
20
+ - !ruby/object:Gem::Version
21
+ version: '3.1'
33
22
  type: :development
34
- version_requirements: *id001
35
- - !ruby/object:Gem::Dependency
36
- name: sqlite3
37
23
  prerelease: false
38
- requirement: &id002 !ruby/object:Gem::Requirement
24
+ version_requirements: !ruby/object:Gem::Requirement
39
25
  none: false
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- hash: 3
44
- segments:
45
- - 0
46
- version: "0"
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.1'
30
+ - !ruby/object:Gem::Dependency
31
+ name: sqlite3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
47
38
  type: :development
48
- version_requirements: *id002
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
49
46
  description: Dump and load relational objects between Ruby environments.
50
47
  email: ryan@github.com
51
- executables:
48
+ executables:
52
49
  - replicate
53
50
  extensions: []
54
-
55
51
  extra_rdoc_files: []
56
-
57
- files:
52
+ files:
58
53
  - COPYING
59
54
  - HACKING
60
55
  - README.md
@@ -72,41 +67,31 @@ files:
72
67
  - test/dumpscript.rb
73
68
  - test/loader_test.rb
74
69
  - test/replicate_test.rb
75
- has_rdoc: true
76
70
  homepage: http://github.com/rtomayko/replicate
77
71
  licenses: []
78
-
79
72
  post_install_message:
80
73
  rdoc_options: []
81
-
82
- require_paths:
74
+ require_paths:
83
75
  - lib
84
- required_ruby_version: !ruby/object:Gem::Requirement
76
+ required_ruby_version: !ruby/object:Gem::Requirement
85
77
  none: false
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- hash: 3
90
- segments:
91
- - 0
92
- version: "0"
93
- required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
83
  none: false
95
- requirements:
96
- - - ">="
97
- - !ruby/object:Gem::Version
98
- hash: 3
99
- segments:
100
- - 0
101
- version: "0"
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
102
88
  requirements: []
103
-
104
89
  rubyforge_project:
105
- rubygems_version: 1.6.2
90
+ rubygems_version: 1.8.23
106
91
  signing_key:
107
92
  specification_version: 2
108
93
  summary: Dump and load relational objects between Ruby environments.
109
- test_files:
94
+ test_files:
110
95
  - test/active_record_test.rb
111
96
  - test/dumper_test.rb
112
97
  - test/loader_test.rb