replicate 1.3 → 1.4
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 +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
|