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 +14 -2
- data/lib/replicate/active_record.rb +86 -29
- data/lib/replicate/dumper.rb +8 -1
- data/lib/replicate/object.rb +1 -1
- data/test/active_record_test.rb +53 -0
- metadata +46 -61
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
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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
|
data/lib/replicate/dumper.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/replicate/object.rb
CHANGED
data/test/active_record_test.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
25
|
none: false
|
40
|
-
requirements:
|
41
|
-
- -
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
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
|
-
|
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.
|
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
|