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 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