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 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 = $stdout
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.reflect_on_all_associations(:belongs_to).each do |reflection|
39
- foreign_key = (reflection.options[:foreign_key] || "#{reflection.name}_id").to_s
40
- if id = attributes[foreign_key]
41
- attributes[foreign_key] = [:id, reflection.klass.to_s, id]
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
@@ -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 just evals the source of the file in the context
53
- # of the dumper. Dump scripts are useful when you want to dump a lot 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
- instance_eval File.read(file), file, 0
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
@@ -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
- type, value = value[1].to_s, value[2]
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[type][remote_id]
100
+ if local_id = @keymap[referenced_type][remote_id]
101
101
  local_id
102
102
  else
103
- warn "error: #{type} #{remote_id} missing from keymap"
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
- # Turn a string into an object by traversing constants.
127
- def constantize(string)
128
- namespace = Object
129
- string.split('::').each { |name| namespace = namespace.const_get(name) }
130
- namespace
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
@@ -27,10 +27,7 @@ module Replicate
27
27
  end
28
28
 
29
29
  def normal_log(type, id, attrs, object)
30
- message = " %sing: %4d objects" % [@prefix, @count]
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
@@ -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
- warn "Using activerecord #{ActiveRecord::VERSION::STRING}"
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
- version: '1.3'
5
- prerelease:
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
- date: 2011-09-10 00:00:00.000000000Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
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
- requirement: &70344083570000 !ruby/object:Gem::Requirement
17
- none: false
18
- requirements:
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ requirements:
19
24
  - - ~>
20
- - !ruby/object:Gem::Version
21
- version: '3.1'
25
+ - !ruby/object:Gem::Version
26
+ segments:
27
+ - 3
28
+ - 1
29
+ version: "3.1"
22
30
  type: :development
23
- prerelease: false
24
- version_requirements: *70344083570000
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
- version_requirements: *70344083569500
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
- files:
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
- require_paths:
76
+
77
+ require_paths:
65
78
  - lib
66
- required_ruby_version: !ruby/object:Gem::Requirement
67
- none: false
68
- requirements:
69
- - - ! '>='
70
- - !ruby/object:Gem::Version
71
- version: '0'
72
- required_rubygems_version: !ruby/object:Gem::Requirement
73
- none: false
74
- requirements:
75
- - - ! '>='
76
- - !ruby/object:Gem::Version
77
- version: '0'
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.8.6
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