replicate 1.5 → 1.5.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.
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