replicate 1.3 → 1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +18 -3
- data/bin/replicate +6 -1
- data/lib/replicate/active_record.rb +38 -4
- data/lib/replicate/dumper.rb +9 -4
- data/lib/replicate/loader.rb +30 -11
- data/lib/replicate/status.rb +1 -4
- data/test/active_record_test.rb +111 -1
- metadata +57 -41
data/README.md
CHANGED
@@ -18,7 +18,7 @@ Synopsis
|
|
18
18
|
|
19
19
|
### Dumping objects
|
20
20
|
|
21
|
-
Evaluate a Ruby expression, dumping all resulting to standard output:
|
21
|
+
Evaluate a Ruby expression, dumping all resulting objects to standard output:
|
22
22
|
|
23
23
|
$ replicate -r ./config/environment -d "User.find(1)" > user.dump
|
24
24
|
==> dumped 4 total objects:
|
@@ -163,6 +163,21 @@ end
|
|
163
163
|
Multiple attribute names may be specified to define a compound key. Foreign key
|
164
164
|
column attributes (`user_id`) are often included in natural keys.
|
165
165
|
|
166
|
+
### Omission of attributes and associations
|
167
|
+
|
168
|
+
You might want to exclude some attributes or associations from being dumped. For
|
169
|
+
this, use the replicate_omit_attributes macro:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class User < ActiveRecord::Base
|
173
|
+
has_one :profile
|
174
|
+
|
175
|
+
replicate_omit_attributes :created_at, :profile
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
You can omit belongs_to associations by omitting the foreign key column.
|
180
|
+
|
166
181
|
### Validations and Callbacks
|
167
182
|
|
168
183
|
__IMPORTANT:__ All ActiveRecord validations and callbacks are disabled on the
|
@@ -206,8 +221,8 @@ class User
|
|
206
221
|
attr_accessor :name, :email
|
207
222
|
|
208
223
|
def dump_replicant(dumper)
|
209
|
-
attributes { 'name' => name, 'email' => email }
|
210
|
-
dumper.write self.class, id, attributes
|
224
|
+
attributes = { 'name' => name, 'email' => email }
|
225
|
+
dumper.write self.class, id, attributes, self
|
211
226
|
end
|
212
227
|
end
|
213
228
|
```
|
data/bin/replicate
CHANGED
@@ -23,6 +23,8 @@
|
|
23
23
|
#/ -v, --verbose Write more status output.
|
24
24
|
#/ -q, --quiet Write less status output.
|
25
25
|
$stderr.sync = true
|
26
|
+
$stdout = $stderr
|
27
|
+
|
26
28
|
require 'optparse'
|
27
29
|
|
28
30
|
# default options
|
@@ -30,7 +32,7 @@ mode = nil
|
|
30
32
|
verbose = false
|
31
33
|
quiet = false
|
32
34
|
keep_id = false
|
33
|
-
out =
|
35
|
+
out = STDOUT
|
34
36
|
force = false
|
35
37
|
|
36
38
|
# parse arguments
|
@@ -67,6 +69,9 @@ if mode == :dump
|
|
67
69
|
ARGV.each do |code|
|
68
70
|
if File.exist?(code)
|
69
71
|
dumper.load_script code
|
72
|
+
elsif code == '-'
|
73
|
+
code = $stdin.read
|
74
|
+
objects = dumper.instance_eval(code, '<stdin>', 0)
|
70
75
|
else
|
71
76
|
objects = dumper.instance_eval(code, '<argv>', 0)
|
72
77
|
dumper.dump objects
|
@@ -35,10 +35,25 @@ module Replicate
|
|
35
35
|
# version of the same object.
|
36
36
|
def replicant_attributes
|
37
37
|
attributes = self.attributes.dup
|
38
|
-
self.class.
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
self.class.replicate_omit_attributes.each do |omit|
|
39
|
+
attributes.delete(omit.to_s)
|
40
|
+
end
|
41
|
+
self.class.reflect_on_all_associations(:belongs_to).select {|association|
|
42
|
+
association.options[:polymorphic] != true
|
43
|
+
}.each do |reflection|
|
44
|
+
klass = reflection.klass
|
45
|
+
options = reflection.options
|
46
|
+
primary_key = (options[:primary_key] || klass.primary_key).to_s
|
47
|
+
foreign_key = (options[:foreign_key] || "#{reflection.name}_id").to_s
|
48
|
+
if primary_key == klass.primary_key
|
49
|
+
if id = attributes[foreign_key]
|
50
|
+
attributes[foreign_key] = [:id, reflection.klass.to_s, id]
|
51
|
+
else
|
52
|
+
# nil value in association reference
|
53
|
+
end
|
54
|
+
else
|
55
|
+
# association uses non-primary-key foreign key. no special key
|
56
|
+
# conversion needed.
|
42
57
|
end
|
43
58
|
end
|
44
59
|
attributes
|
@@ -59,6 +74,7 @@ module Replicate
|
|
59
74
|
# Returns nothing.
|
60
75
|
def dump_all_association_replicants(dumper, association_type)
|
61
76
|
self.class.reflect_on_all_associations(association_type).each do |reflection|
|
77
|
+
next if self.class.replicate_omit_attributes.include?(reflection.name)
|
62
78
|
next if (dependent = __send__(reflection.name)).nil?
|
63
79
|
case dependent
|
64
80
|
when ActiveRecord::Base, Array
|
@@ -141,6 +157,23 @@ module Replicate
|
|
141
157
|
@replicate_id = boolean
|
142
158
|
end
|
143
159
|
|
160
|
+
# Set which, if any, attributes should not be dumped. Also works for
|
161
|
+
# associations.
|
162
|
+
#
|
163
|
+
# attribute_names - Macro style setter.
|
164
|
+
def replicate_omit_attributes(*attribute_names)
|
165
|
+
self.replicate_omit_attributes = attribute_names if attribute_names.any?
|
166
|
+
@replicate_omit_attributes || superclass.replicate_omit_attributes
|
167
|
+
end
|
168
|
+
|
169
|
+
# Set which, if any, attributes should not be dumped. Also works for
|
170
|
+
# associations.
|
171
|
+
#
|
172
|
+
# attribute_names - Array of attribute name symbols
|
173
|
+
def replicate_omit_attributes=(attribute_names)
|
174
|
+
@replicate_omit_attributes = attribute_names
|
175
|
+
end
|
176
|
+
|
144
177
|
# Load an individual record into the database. If the models defines a
|
145
178
|
# replicate_natural_key then an existing record will be updated if found
|
146
179
|
# instead of a new record being created.
|
@@ -276,6 +309,7 @@ module Replicate
|
|
276
309
|
::ActiveRecord::Base.send :extend, ClassMethods
|
277
310
|
::ActiveRecord::Base.replicate_associations = []
|
278
311
|
::ActiveRecord::Base.replicate_natural_key = []
|
312
|
+
::ActiveRecord::Base.replicate_omit_attributes = []
|
279
313
|
::ActiveRecord::Base.replicate_id = false
|
280
314
|
end
|
281
315
|
end
|
data/lib/replicate/dumper.rb
CHANGED
@@ -49,11 +49,16 @@ module Replicate
|
|
49
49
|
use Replicate::Status, 'dump', out, verbose, quiet
|
50
50
|
end
|
51
51
|
|
52
|
-
# Load a dump script. This
|
53
|
-
# of
|
54
|
-
# stuff.
|
52
|
+
# Load a dump script. This evals the source of the file in the context
|
53
|
+
# of a special object with a #dump method that forwards to this instance.
|
54
|
+
# Dump scripts are useful when you want to dump a lot of stuff. Call the
|
55
|
+
# dump method as many times as necessary to dump all objects.
|
55
56
|
def load_script(file)
|
56
|
-
|
57
|
+
dumper = self
|
58
|
+
object = ::Object.new
|
59
|
+
meta = (class<<object;self;end)
|
60
|
+
meta.send(:define_method, :dump) { |*args| dumper.dump(*args) }
|
61
|
+
object.instance_eval File.read(file), file, 0
|
57
62
|
end
|
58
63
|
|
59
64
|
# Dump one or more objects to the internal array or provided dump
|
data/lib/replicate/loader.rb
CHANGED
@@ -71,7 +71,7 @@ module Replicate
|
|
71
71
|
# Returns the new object instance.
|
72
72
|
def load(type, id, attributes)
|
73
73
|
model_class = constantize(type)
|
74
|
-
translate_ids attributes
|
74
|
+
translate_ids type, id, attributes
|
75
75
|
begin
|
76
76
|
new_id, instance = model_class.load_replicant(type, id, attributes)
|
77
77
|
rescue => boom
|
@@ -91,16 +91,17 @@ module Replicate
|
|
91
91
|
# ... }
|
92
92
|
# These values are translated to local system ids. All object
|
93
93
|
# references must be loaded prior to the referencing object.
|
94
|
-
def translate_ids(attributes)
|
94
|
+
def translate_ids(type, id, attributes)
|
95
95
|
attributes.each do |key, value|
|
96
96
|
next unless value.is_a?(Array) && value[0] == :id
|
97
|
-
|
97
|
+
referenced_type, value = value[1].to_s, value[2]
|
98
98
|
local_ids =
|
99
99
|
Array(value).map do |remote_id|
|
100
|
-
if local_id = @keymap[
|
100
|
+
if local_id = @keymap[referenced_type][remote_id]
|
101
101
|
local_id
|
102
102
|
else
|
103
|
-
warn "
|
103
|
+
warn "warn: #{referenced_type}(#{remote_id}) not in keymap, " +
|
104
|
+
"referenced by #{type}(#{id})##{key}"
|
104
105
|
end
|
105
106
|
end
|
106
107
|
if value.is_a?(Array)
|
@@ -122,12 +123,30 @@ module Replicate
|
|
122
123
|
end
|
123
124
|
end
|
124
125
|
|
125
|
-
|
126
|
-
#
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
126
|
+
# Turn a string into an object by traversing constants. Identical to
|
127
|
+
# ActiveSupport's String#constantize implementation.
|
128
|
+
if Module.method(:const_get).arity == 1
|
129
|
+
# 1.8 implementation doesn't have the inherit argument to const_defined?
|
130
|
+
def constantize(string)
|
131
|
+
string.split('::').inject ::Object do |namespace, name|
|
132
|
+
if namespace.const_defined?(name)
|
133
|
+
namespace.const_get(name)
|
134
|
+
else
|
135
|
+
namespace.const_missing(name)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
else
|
140
|
+
# 1.9 implement has the inherit argument to const_defined?. Use it!
|
141
|
+
def constantize(string)
|
142
|
+
string.split('::').inject ::Object do |namespace, name|
|
143
|
+
if namespace.const_defined?(name, false)
|
144
|
+
namespace.const_get(name)
|
145
|
+
else
|
146
|
+
namespace.const_missing(name)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
131
150
|
end
|
132
151
|
end
|
133
152
|
end
|
data/lib/replicate/status.rb
CHANGED
@@ -27,10 +27,7 @@ module Replicate
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def normal_log(type, id, attrs, object)
|
30
|
-
|
31
|
-
dots = '.' * (@count % 50)
|
32
|
-
dots = ' ' * 50 if dots.empty?
|
33
|
-
@out.write "#{message} #{dots}\r"
|
30
|
+
@out.write " %sing: %4d objects \r" % [@prefix, @count]
|
34
31
|
end
|
35
32
|
|
36
33
|
def complete
|
data/test/active_record_test.rb
CHANGED
@@ -6,7 +6,8 @@ version = ENV['AR_VERSION']
|
|
6
6
|
gem 'activerecord', "~> #{version}" if version
|
7
7
|
require 'active_record'
|
8
8
|
require 'active_record/version'
|
9
|
-
|
9
|
+
version = ActiveRecord::VERSION::STRING
|
10
|
+
warn "Using activerecord #{version}"
|
10
11
|
|
11
12
|
# replicate must be loaded after AR
|
12
13
|
require 'replicate'
|
@@ -36,12 +37,28 @@ ActiveRecord::Schema.define do
|
|
36
37
|
t.string "email"
|
37
38
|
t.datetime "created_at"
|
38
39
|
end
|
40
|
+
|
41
|
+
if version[0,3] > '2.2'
|
42
|
+
create_table "domains", :force => true do |t|
|
43
|
+
t.string "host"
|
44
|
+
end
|
45
|
+
|
46
|
+
create_table "web_pages", :force => true do |t|
|
47
|
+
t.string "url"
|
48
|
+
t.string "domain_host"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
create_table "notes", :force => true do |t|
|
53
|
+
t.integer "notable_id"
|
54
|
+
end
|
39
55
|
end
|
40
56
|
|
41
57
|
# models
|
42
58
|
class User < ActiveRecord::Base
|
43
59
|
has_one :profile, :dependent => :destroy
|
44
60
|
has_many :emails, :dependent => :destroy, :order => 'id'
|
61
|
+
has_many :notes, :as => :notable
|
45
62
|
replicate_natural_key :login
|
46
63
|
end
|
47
64
|
|
@@ -55,6 +72,20 @@ class Email < ActiveRecord::Base
|
|
55
72
|
replicate_natural_key :user_id, :email
|
56
73
|
end
|
57
74
|
|
75
|
+
if version[0,3] > '2.2'
|
76
|
+
class WebPage < ActiveRecord::Base
|
77
|
+
belongs_to :domain, :foreign_key => 'domain_host', :primary_key => 'host'
|
78
|
+
end
|
79
|
+
|
80
|
+
class Domain < ActiveRecord::Base
|
81
|
+
replicate_natural_key :host
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Note < ActiveRecord::Base
|
86
|
+
belongs_to :notable, :polymorphic => true
|
87
|
+
end
|
88
|
+
|
58
89
|
# The test case loads some fixture data once and uses transaction rollback to
|
59
90
|
# reset fixture state for each test's setup.
|
60
91
|
class ActiveRecordTest < Test::Unit::TestCase
|
@@ -92,6 +123,11 @@ class ActiveRecordTest < Test::Unit::TestCase
|
|
92
123
|
|
93
124
|
user = User.create! :login => 'tmm1'
|
94
125
|
user.create_profile :name => 'tmm1', :homepage => 'https://github.com/tmm1'
|
126
|
+
|
127
|
+
if defined?(Domain)
|
128
|
+
github = Domain.create! :host => 'github.com'
|
129
|
+
github_about_page = WebPage.create! :url => 'http://github.com/about', :domain => github
|
130
|
+
end
|
95
131
|
end
|
96
132
|
|
97
133
|
def test_extension_modules_loaded
|
@@ -122,6 +158,55 @@ class ActiveRecordTest < Test::Unit::TestCase
|
|
122
158
|
assert_equal rtomayko.profile, obj
|
123
159
|
end
|
124
160
|
|
161
|
+
def test_omit_dumping_of_attribute
|
162
|
+
objects = []
|
163
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
164
|
+
|
165
|
+
User.replicate_omit_attributes :created_at
|
166
|
+
rtomayko = User.find_by_login('rtomayko')
|
167
|
+
@dumper.dump rtomayko
|
168
|
+
|
169
|
+
assert_equal 2, objects.size
|
170
|
+
|
171
|
+
type, id, attrs, obj = objects.shift
|
172
|
+
assert_equal nil, attrs['created_at']
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_omit_dumping_of_association
|
176
|
+
objects = []
|
177
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
178
|
+
|
179
|
+
User.replicate_omit_attributes :profile
|
180
|
+
rtomayko = User.find_by_login('rtomayko')
|
181
|
+
@dumper.dump rtomayko
|
182
|
+
|
183
|
+
assert_equal 1, objects.size
|
184
|
+
|
185
|
+
type, id, attrs, obj = objects.shift
|
186
|
+
assert_equal 'User', type
|
187
|
+
end
|
188
|
+
|
189
|
+
if ActiveRecord::VERSION::STRING[0, 3] > '2.2'
|
190
|
+
def test_dump_and_load_non_standard_foreign_key_association
|
191
|
+
objects = []
|
192
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
193
|
+
|
194
|
+
github_about_page = WebPage.find_by_url('http://github.com/about')
|
195
|
+
assert_equal "github.com", github_about_page.domain.host
|
196
|
+
@dumper.dump github_about_page
|
197
|
+
|
198
|
+
WebPage.delete_all
|
199
|
+
Domain.delete_all
|
200
|
+
|
201
|
+
# load everything back up
|
202
|
+
objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs }
|
203
|
+
|
204
|
+
github_about_page = WebPage.find_by_url('http://github.com/about')
|
205
|
+
assert_equal "github.com", github_about_page.domain_host
|
206
|
+
assert_equal "github.com", github_about_page.domain.host
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
125
210
|
def test_auto_dumping_has_one_associations
|
126
211
|
objects = []
|
127
212
|
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
@@ -146,6 +231,31 @@ class ActiveRecordTest < Test::Unit::TestCase
|
|
146
231
|
assert_equal rtomayko.profile, obj
|
147
232
|
end
|
148
233
|
|
234
|
+
def test_auto_dumping_does_not_fail_on_polymorphic_associations
|
235
|
+
objects = []
|
236
|
+
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
237
|
+
|
238
|
+
rtomayko = User.find_by_login('rtomayko')
|
239
|
+
note = Note.create!(:notable => rtomayko)
|
240
|
+
@dumper.dump note
|
241
|
+
|
242
|
+
assert_equal 3, objects.size
|
243
|
+
|
244
|
+
type, id, attrs, obj = objects.shift
|
245
|
+
assert_equal 'User', type
|
246
|
+
assert_equal rtomayko.id, id
|
247
|
+
|
248
|
+
type, id, attrs, obj = objects.shift
|
249
|
+
assert_equal 'Profile', type
|
250
|
+
|
251
|
+
type, id, attrs, obj = objects.shift
|
252
|
+
assert_equal 'Note', type
|
253
|
+
assert_equal note.id, id
|
254
|
+
assert_equal note.notable_type, attrs['notable_type']
|
255
|
+
assert_equal attrs["notable_id"], rtomayko.id
|
256
|
+
assert_equal note, obj
|
257
|
+
end
|
258
|
+
|
149
259
|
def test_dumping_has_many_associations
|
150
260
|
objects = []
|
151
261
|
@dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] }
|
metadata
CHANGED
@@ -1,45 +1,55 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: replicate
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 4
|
8
|
+
version: "1.4"
|
6
9
|
platform: ruby
|
7
|
-
authors:
|
10
|
+
authors:
|
8
11
|
- Ryan Tomayko
|
9
12
|
autorequire:
|
10
13
|
bindir: bin
|
11
14
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
+
|
16
|
+
date: 2011-10-19 00:00:00 -07:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
15
20
|
name: activerecord
|
16
|
-
|
17
|
-
|
18
|
-
requirements:
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
19
24
|
- - ~>
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 3
|
28
|
+
- 1
|
29
|
+
version: "3.1"
|
22
30
|
type: :development
|
23
|
-
|
24
|
-
|
25
|
-
- !ruby/object:Gem::Dependency
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
26
33
|
name: sqlite3
|
27
|
-
requirement: &70344083569500 !ruby/object:Gem::Requirement
|
28
|
-
none: false
|
29
|
-
requirements:
|
30
|
-
- - ! '>='
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '0'
|
33
|
-
type: :development
|
34
34
|
prerelease: false
|
35
|
-
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :development
|
43
|
+
version_requirements: *id002
|
36
44
|
description: Dump and load relational objects between Ruby environments.
|
37
45
|
email: ryan@github.com
|
38
|
-
executables:
|
46
|
+
executables:
|
39
47
|
- replicate
|
40
48
|
extensions: []
|
49
|
+
|
41
50
|
extra_rdoc_files: []
|
42
|
-
|
51
|
+
|
52
|
+
files:
|
43
53
|
- COPYING
|
44
54
|
- HACKING
|
45
55
|
- README.md
|
@@ -57,31 +67,37 @@ files:
|
|
57
67
|
- test/dumpscript.rb
|
58
68
|
- test/loader_test.rb
|
59
69
|
- test/replicate_test.rb
|
70
|
+
has_rdoc: true
|
60
71
|
homepage: http://github.com/rtomayko/replicate
|
61
72
|
licenses: []
|
73
|
+
|
62
74
|
post_install_message:
|
63
75
|
rdoc_options: []
|
64
|
-
|
76
|
+
|
77
|
+
require_paths:
|
65
78
|
- lib
|
66
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
requirements:
|
75
|
-
- -
|
76
|
-
- !ruby/object:Gem::Version
|
77
|
-
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
78
93
|
requirements: []
|
94
|
+
|
79
95
|
rubyforge_project:
|
80
|
-
rubygems_version: 1.
|
96
|
+
rubygems_version: 1.3.6
|
81
97
|
signing_key:
|
82
98
|
specification_version: 2
|
83
99
|
summary: Dump and load relational objects between Ruby environments.
|
84
|
-
test_files:
|
100
|
+
test_files:
|
85
101
|
- test/active_record_test.rb
|
86
102
|
- test/dumper_test.rb
|
87
103
|
- test/loader_test.rb
|