├── .gitignore ├── COPYING ├── HACKING ├── README.md ├── Rakefile ├── bin └── replicate ├── examples └── ar-3.x │ ├── .gitignore │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Rakefile │ ├── app │ └── models │ │ ├── .gitkeep │ │ ├── city.rb │ │ ├── country.rb │ │ └── language.rb │ ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ └── locales │ │ └── en.yml │ ├── db │ ├── migrate │ │ ├── 20110909181231_create_world_schema.rb │ │ ├── 20110909184144_railsize_foreign_key_columns.rb │ │ ├── 20110909190458_railsize_countries_language_country_id.rb │ │ └── 20110909194842_set_country_ids.rb │ ├── schema.rb │ ├── seeds.rb │ └── world.sqlite3 │ ├── log │ └── .gitkeep │ └── script │ └── rails ├── lib ├── replicate.rb └── replicate │ ├── active_record.rb │ ├── dumper.rb │ ├── emitter.rb │ ├── loader.rb │ ├── object.rb │ └── status.rb ├── replicate.gemspec └── test ├── active_record_test.rb ├── custom_objects_test.rb ├── dumper_test.rb ├── dumpscript.rb ├── linked_dumpscript.rb ├── loader_test.rb └── replicate_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /test/db 2 | /vendor 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ryan Tomayko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Grab a local clone of rtomayko/replicate: 2 | 3 | git clone git://github.com/rtomayko/replicate.git 4 | cd replicate 5 | 6 | The default rake task installs the latest supported activerecord 7 | version to a project local GEM_HOME=./vendor and runs the unit 8 | tests: 9 | 10 | $ rake 11 | installing activerecord-3.1.0 to ./vendor/1.8 12 | installing sqlite3 to ./vendor/1.8 13 | Using activerecord 3.1.0 14 | Loaded suite ... 15 | Started 16 | .................... 17 | Finished in 0.150186 seconds. 18 | 19 | 20 tests, 106 assertions, 0 failures, 0 errors 20 | 21 | Use `rake test:all' to run tests under all activerecord versions: 22 | 23 | $ rake test:all 24 | installing activerecord ~> 2.2.3 to ./vendor 25 | installing activerecord ~> 2.3.14 to ./vendor 26 | installing activerecord ~> 3.0.10 to ./vendor 27 | installing activerecord ~> 3.1.0 to ./vendor 28 | ==> testing activerecord ~> 2.2.3 29 | Started 30 | .................... 31 | Finished in 0.119517 seconds. 32 | 33 | 20 tests, 106 assertions, 0 failures, 0 errors 34 | ==> testing activerecord ~> 2.3.14 35 | Started 36 | .................... 37 | Finished in 0.119517 seconds. 38 | 39 | 20 tests, 106 assertions, 0 failures, 0 errors 40 | 41 | 42 | rake test:all should always be passing under latest stable MRI 43 | 1.8.7 and MRI 1.9.x. 44 | 45 | Running individual test files directly requires setting the 46 | GEM_HOME environment variable and ensuring ./lib is on the load 47 | path: 48 | 49 | export GEM_HOME=vendor/1.9 # or 1.8.7 50 | ruby -Ilib test/active_record_test.rb 51 | 52 | You can also control which activerecord version is used in the 53 | test with the AR_VERSION environment variable: 54 | 55 | rake setup:all 56 | export GEM_HOME=vendor/1.8.7 57 | AR_VERSION=3.1.0 ruby -rubygems -Ilib test/active_record_test.rb 58 | 59 | If you have something worth sharing, please send a pull request: 60 | 61 | https://github.com/rtomayko/replicate 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dump and load relational objects between Ruby environments. 2 | =========================================================== 3 | 4 | The project started at GitHub to simplify the process of getting real production 5 | data into development and staging environments. We use it to replicate entire 6 | repository data (including associated issue, pull request, commit comment, etc. 7 | records) from production to our development environments with a single command. 8 | It's excessively useful for troubleshooting issues, support requests, and 9 | exception reports as well as for establishing real data for evaluating design 10 | concepts. 11 | 12 | Synopsis 13 | -------- 14 | 15 | ### Installing 16 | 17 | $ gem install replicate 18 | 19 | ### Dumping objects 20 | 21 | Evaluate a Ruby expression, dumping all resulting objects to standard output: 22 | 23 | $ replicate -r ./config/environment -d "User.find(1)" > user.dump 24 | ==> dumped 4 total objects: 25 | Profile 1 26 | User 1 27 | UserEmail 2 28 | 29 | The `-r ./config/environment` option is used to require environment setup and 30 | model instantiation code needed by the ruby expression. 31 | 32 | ### Dumping many objects with a dump script 33 | 34 | Dump scripts are normal ruby source files evaluated in the context of the 35 | dumper. The `dump(object)` method is used to put objects into the dump stream. 36 | 37 | ```ruby 38 | # config/replicate/dump-stuff.rb 39 | require 'config/environment' 40 | 41 | %w[rtomayko/tilt rtomayko/bcat].each do |repo_name| 42 | repo = Repository.find_by_name_with_owner(repo_name) 43 | dump repo 44 | dump repo.commit_comments 45 | dump repo.issues 46 | end 47 | ``` 48 | 49 | Run the dump script: 50 | 51 | $ replicate -d config/replicate/dump-stuff.rb > repos.dump 52 | ==> dumped 1479 total objects: 53 | AR::Habtm 101 54 | CommitComment 95 55 | Issue 101 56 | IssueComment 427 57 | IssueEvent 308 58 | Label 5 59 | Language 19 60 | LanguageName 1 61 | Milestone 3 62 | Organization 4 63 | Profile 82 64 | PullRequest 44 65 | PullRequestReviewComment 8 66 | Repository 20 67 | Team 4 68 | TeamMember 6 69 | User 89 70 | UserEmail 162 71 | 72 | ### Loading many objects: 73 | 74 | $ replicate -r ./config/environment -l < repos.dump 75 | ==> loaded 1479 total objects: 76 | AR::Habtm 101 77 | CommitComment 95 78 | Issue 101 79 | IssueComment 427 80 | IssueEvent 308 81 | Label 5 82 | Language 19 83 | LanguageName 1 84 | Milestone 3 85 | Organization 4 86 | Profile 82 87 | PullRequest 44 88 | PullRequestReviewComment 8 89 | Repository 20 90 | Team 4 91 | TeamMember 6 92 | User 89 93 | UserEmail 162 94 | 95 | ### Dumping and loading over ssh 96 | 97 | $ remote_command="replicate -r /app/config/environment -d 'User.find(1234)'" 98 | $ ssh example.org "$remote_command" |replicate -r ./config/environment -l 99 | 100 | ActiveRecord 101 | ------------ 102 | 103 | Basic support for dumping and loading ActiveRecord objects is included. The 104 | tests pass under ActiveRecord versions 2.2.3, 2.3.5, 2.3.14, 3.0.10, 3.1.0, and 3.2.0 under 105 | MRI 1.8.7 as well as under MRI 1.9.2. 106 | 107 | To use customization macros in your models, require the replicate library after 108 | ActiveRecord (in e.g., `config/initializers/libraries.rb`): 109 | 110 | ```ruby 111 | require 'active_record' 112 | require 'replicate' 113 | ``` 114 | 115 | ActiveRecord support works sensibly without customization so this isn't strictly 116 | necessary to use the `replicate` command. The following sections document the 117 | available customization macros. 118 | 119 | ### Association Dumping 120 | 121 | The baked in support adds some more or less sensible default behavior for all 122 | subclasses of `ActiveRecord::Base` such that dumping an object will bring in 123 | objects related via `belongs_to` and `has_one` associations. 124 | 125 | Unlike 1:1 associations, `has_many` and `has_and_belongs_to_many` associations 126 | are not automatically included. Doing so would quickly lead to the entire 127 | database being sucked in. It can be useful to mark specific associations for 128 | automatic inclusion using the `replicate_associations` macro. For instance, 129 | to always include `EmailAddress` records belonging to a `User`: 130 | 131 | ```ruby 132 | class User < ActiveRecord::Base 133 | belongs_to :profile 134 | has_many :email_addresses 135 | 136 | replicate_associations :email_addresses 137 | end 138 | ``` 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 | 146 | ### Natural Keys 147 | 148 | By default, the loader attempts to create a new record with a new primary key id 149 | for all objects. This can lead to unique constraint errors when a record already 150 | exists with matching attributes. To update existing records instead of 151 | creating new ones, define a natural key for the model using the `replicate_natural_key` 152 | macro: 153 | 154 | ```ruby 155 | class User < ActiveRecord::Base 156 | belongs_to :profile 157 | has_many :email_addresses 158 | 159 | replicate_natural_key :login 160 | replicate_associations :email_addresses 161 | end 162 | 163 | class EmailAddress < ActiveRecord::Base 164 | belongs_to :user 165 | replicate_natural_key :user_id, :email 166 | end 167 | ``` 168 | 169 | Multiple attribute names may be specified to define a compound key. Foreign key 170 | column attributes (`user_id`) are often included in natural keys. 171 | 172 | ### Omission of attributes and associations 173 | 174 | You might want to exclude some attributes or associations from being dumped. For 175 | this, use the replicate_omit_attributes macro: 176 | 177 | ```ruby 178 | class User < ActiveRecord::Base 179 | has_one :profile 180 | 181 | replicate_omit_attributes :created_at, :profile 182 | end 183 | ``` 184 | 185 | You can omit belongs_to associations by omitting the foreign key column. 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 | 193 | ### Validations and Callbacks 194 | 195 | __IMPORTANT:__ All ActiveRecord validations and callbacks are disabled on the 196 | loading side. While replicate piggybacks on AR for relationship information and 197 | uses `ActiveRecord::Base#save` to write objects to the database, it's designed 198 | to act as a simple dump / load tool. 199 | 200 | It's sometimes useful to run certain types of callbacks on replicate. For 201 | instance, you might want to create files on disk or load information into a 202 | separate data store any time an object enters the database. The best way to go 203 | about this currently is to override the model's `load_replicant` class method: 204 | 205 | ```ruby 206 | class User < ActiveRecord::Base 207 | def self.load_replicant(type, id, attrs) 208 | id, object = super 209 | object.register_in_redis 210 | object.some_other_callback 211 | [id, object] 212 | end 213 | end 214 | ``` 215 | 216 | This interface will be improved in future versions. 217 | 218 | Custom Objects 219 | -------------- 220 | 221 | Other object types may be included in the dump stream so long as they implement 222 | the `dump_replicant` and `load_replicant` methods. 223 | 224 | ### dump_replicant 225 | 226 | The dump side calls `#dump_replicant(dumper, opts={})` on each object. The method must 227 | call `dumper.write()` with the class name, id, and hash of primitively typed 228 | attributes for the object: 229 | 230 | ```ruby 231 | class User 232 | attr_reader :id 233 | attr_accessor :name, :email 234 | 235 | def dump_replicant(dumper, opts={}) 236 | attributes = { 'name' => name, 'email' => email } 237 | dumper.write self.class, id, attributes, self 238 | end 239 | end 240 | ``` 241 | 242 | ### load_replicant 243 | 244 | The load side calls `::load_replicant(type, id, attributes)` on the class to 245 | load each object into the current environment. The method must return an 246 | `[id, object]` tuple: 247 | 248 | ```ruby 249 | class User 250 | def self.load_replicant(type, id, attributes) 251 | user = User.new 252 | user.name = attributes['name'] 253 | user.email = attributes['email'] 254 | user.save! 255 | [user.id, user] 256 | end 257 | end 258 | ``` 259 | 260 | How it works 261 | ------------ 262 | 263 | The dump format is designed for streaming relational data. Each object is 264 | encoded as a `[type, id, attributes]` tuple and marshalled directly onto the 265 | stream. The `type` (class name string) and `id` must form a distinct key when 266 | combined, `attributes` must consist of only string keys and simply typed values. 267 | 268 | Relationships between objects in the stream are managed as follows: 269 | 270 | - An object's attributes may encode references to objects that precede it 271 | in the stream using a simple tuple format: `[:id, 'User', 1234]`. 272 | 273 | - The dump side ensures that objects are written to the dump stream in 274 | "reference order" such that when an object A includes a reference attribute 275 | to an object B, B is guaranteed to arrive before A. 276 | 277 | - The load side maintains a mapping of ids from the dumping system to the newly 278 | replicated objects on the loading system. When the loader encounters a 279 | reference value `[:id, 'User', 1234]` in an object's attributes, it converts it 280 | to the load side id value. 281 | 282 | Dumping and loading happens in a streaming fashion. There is no limit on the 283 | number of objects included in the stream. 284 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'rake/clean' 3 | task :default => [:setup, :test] 4 | 5 | vendor_dir = './vendor' 6 | ruby_version = RbConfig::CONFIG['ruby_version'] 7 | ENV['GEM_HOME'] = "#{vendor_dir}/#{ruby_version}" 8 | 9 | desc "Install gem dependencies for development" 10 | task :setup => 'setup:latest' do 11 | verbose(false) do 12 | gem_install 'sqlite3' 13 | gem_install 'test_after_commit' 14 | end 15 | end 16 | 17 | desc "Run tests" 18 | task :test do 19 | ENV['RUBYOPT'] = [ENV['RUBYOPT'], 'rubygems'].compact.join(' ') 20 | ENV['RUBYLIB'] = ['lib', ENV['RUBYLIB']].compact.join(':') 21 | sh "testrb test/*_test.rb", :verbose => false 22 | end 23 | CLEAN.include 'test/db' 24 | 25 | desc "Build gem" 26 | task :build do 27 | sh "gem build replicate.gemspec" 28 | end 29 | 30 | # supported activerecord gem versions 31 | AR_VERSIONS = [] 32 | AR_VERSIONS.concat %w[2.2.3 2.3.5] if RUBY_VERSION < '1.9' 33 | AR_VERSIONS.concat %w[2.3.14 3.0.10 3.1.0 3.2.0] 34 | 35 | desc "Run unit tests under all supported AR versions" 36 | task 'test:all' => 'setup:all' do 37 | failures = [] 38 | AR_VERSIONS.each do |vers| 39 | warn "==> testing activerecord ~> #{vers}" 40 | ENV['AR_VERSION'] = vers 41 | ok = system("rake -s test") 42 | failures << vers if !ok 43 | warn '' 44 | end 45 | fail "activerecord version failures: #{failures.join(', ')}" if failures.any? 46 | end 47 | 48 | # file tasks for installing each AR version 49 | desc 'Install gem dependencies for all supported AR versions' 50 | task 'setup:all' => 'setup' 51 | AR_VERSIONS.each do |vers| 52 | version_file = "#{ENV['GEM_HOME']}/versions/activerecord-#{vers}" 53 | file version_file do |f| 54 | verbose(false) { gem_install 'activerecord', vers } 55 | end 56 | task "setup:#{vers}" => version_file 57 | task "setup:all" => "setup:#{vers}" 58 | end 59 | task "setup:latest" => "setup:#{AR_VERSIONS.last}" 60 | CLEAN.include 'vendor' 61 | 62 | # Install a gem to the local GEM_HOME but only if it isn't already installed 63 | def gem_install(name, version = nil) 64 | version_name = [name, version].compact.join('-') 65 | version_file = "#{ENV['GEM_HOME']}/versions/#{version_name}" 66 | return if File.exist?(version_file) 67 | warn "installing #{version_name} to #{ENV['GEM_HOME']}" 68 | command = "gem install --no-rdoc --no-ri #{name}" 69 | command += " -v '~> #{version}'" if version 70 | command += " >/dev/null" 71 | sh command 72 | mkdir_p File.dirname(version_file) 73 | File.open(version_file, 'wb') { } 74 | end 75 | -------------------------------------------------------------------------------- /bin/replicate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #/ Usage: replicate --dump dumpscript.rb > objects.dump 3 | #/ replicate [-r ] --dump "" [-- ...] > objects.dump 4 | #/ replicate [-r ] --load < objects.dump 5 | #/ Dump and load objects between environments. 6 | # 7 | #/ The --dump form writes to stdout the objects dumped by the script or 8 | #/ ruby expression(s) given. Dump scripts are normal Ruby source files but 9 | #/ must call dump(object) on one or more objects. When a Ruby expression is 10 | #/ given, all resulting objects are dumped automatically. 11 | #/ 12 | #/ Dump scripts have access to any additional provided via the normal 13 | #/ system ARGV array. This can be used to pass arguments to dump scripts. 14 | #/ 15 | #/ The --load form reads dump data from stdin and creates objects under the 16 | #/ current environment. 17 | #/ 18 | #/ Mode selection: 19 | #/ -d, --dump Dump the repository and all related objects to stdout. 20 | #/ -l, --load Load dump file data from stdin. 21 | #/ 22 | #/ Options: 23 | #/ -r, --require Require the library. Often used with 'config/environment'. 24 | #/ -i, --keep-id Use replicated ids when loading dump file. 25 | #/ -f, --force Allow loading in production environments. 26 | #/ -v, --verbose Write more status output. 27 | #/ -q, --quiet Write less status output. 28 | $stderr.sync = true 29 | $stdout = $stderr 30 | 31 | require 'optparse' 32 | 33 | # default options 34 | mode = nil 35 | verbose = false 36 | quiet = false 37 | keep_id = false 38 | out = STDOUT 39 | force = false 40 | 41 | # parse arguments 42 | file = __FILE__ 43 | usage = lambda { exec "grep ^#/<'#{file}'|cut -c4-" } 44 | original_argv = ARGV.dup 45 | ARGV.options do |opts| 46 | opts.on("-d", "--dump") { mode = :dump } 47 | opts.on("-l", "--load") { mode = :load } 48 | opts.on("-r", "--require=f") { |file| require file } 49 | opts.on("-v", "--verbose") { verbose = true } 50 | opts.on("-q", "--quiet") { quiet = true } 51 | opts.on("-i", "--keep-id") { keep_id = true } 52 | opts.on("--force") { force = true } 53 | opts.on_tail("-h", "--help", &usage) 54 | opts.parse! 55 | end 56 | 57 | # load replicate lib and setup AR 58 | require 'replicate' 59 | if defined?(ActiveRecord::Base) 60 | require 'replicate/active_record' 61 | ActiveRecord::Base.replicate_id = keep_id 62 | ActiveRecord::Base.connection.enable_query_cache! 63 | end 64 | 65 | # dump mode means we're reading records from the database here and writing to 66 | # stdout. the database should not be modified at all by this operation. 67 | if mode == :dump 68 | script = ARGV.shift 69 | usage.call if script.empty? 70 | Replicate::Dumper.new do |dumper| 71 | dumper.marshal_to out 72 | dumper.log_to $stderr, verbose, quiet 73 | if script == '-' 74 | code = $stdin.read 75 | objects = dumper.instance_eval(code, '', 0) 76 | elsif File.exist?(script) 77 | dumper.load_script script 78 | else 79 | objects = dumper.instance_eval(script, '', 0) 80 | dumper.dump objects 81 | end 82 | end 83 | 84 | # load mode means we're reading objects from stdin and creating them under 85 | # the current environment. 86 | elsif mode == :load 87 | if Replicate.production_environment? && !force 88 | abort "error: refusing to load in production environment\n" + 89 | " manual override: #{File.basename($0)} --force #{original_argv.join(' ')}" 90 | else 91 | Replicate::Loader.new do |loader| 92 | loader.log_to $stderr, verbose, quiet 93 | loader.read $stdin 94 | end 95 | end 96 | 97 | # mode not set means no -l or -d arg was given. show usage and bail. 98 | else 99 | usage.call 100 | end 101 | -------------------------------------------------------------------------------- /examples/ar-3.x/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/ 5 | .sass-cache/ 6 | -------------------------------------------------------------------------------- /examples/ar-3.x/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails', '3.1.0' 4 | gem 'mysql2' 5 | 6 | # Bundle edge Rails instead: 7 | # gem 'rails', :git => 'git://github.com/rails/rails.git' 8 | 9 | gem 'sqlite3' 10 | -------------------------------------------------------------------------------- /examples/ar-3.x/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | actionmailer (3.1.0) 5 | actionpack (= 3.1.0) 6 | mail (~> 2.3.0) 7 | actionpack (3.1.0) 8 | activemodel (= 3.1.0) 9 | activesupport (= 3.1.0) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | i18n (~> 0.6) 13 | rack (~> 1.3.2) 14 | rack-cache (~> 1.0.3) 15 | rack-mount (~> 0.8.2) 16 | rack-test (~> 0.6.1) 17 | sprockets (~> 2.0.0) 18 | activemodel (3.1.0) 19 | activesupport (= 3.1.0) 20 | bcrypt-ruby (~> 3.0.0) 21 | builder (~> 3.0.0) 22 | i18n (~> 0.6) 23 | activerecord (3.1.0) 24 | activemodel (= 3.1.0) 25 | activesupport (= 3.1.0) 26 | arel (~> 2.2.1) 27 | tzinfo (~> 0.3.29) 28 | activeresource (3.1.0) 29 | activemodel (= 3.1.0) 30 | activesupport (= 3.1.0) 31 | activesupport (3.1.0) 32 | multi_json (~> 1.0) 33 | arel (2.2.1) 34 | bcrypt-ruby (3.0.0) 35 | builder (3.0.0) 36 | erubis (2.7.0) 37 | hike (1.2.1) 38 | i18n (0.6.0) 39 | mail (2.3.0) 40 | i18n (>= 0.4.0) 41 | mime-types (~> 1.16) 42 | treetop (~> 1.4.8) 43 | mime-types (1.16) 44 | multi_json (1.0.3) 45 | mysql2 (0.3.7) 46 | polyglot (0.3.2) 47 | rack (1.3.2) 48 | rack-cache (1.0.3) 49 | rack (>= 0.4) 50 | rack-mount (0.8.3) 51 | rack (>= 1.0.0) 52 | rack-ssl (1.3.2) 53 | rack 54 | rack-test (0.6.1) 55 | rack (>= 1.0) 56 | rails (3.1.0) 57 | actionmailer (= 3.1.0) 58 | actionpack (= 3.1.0) 59 | activerecord (= 3.1.0) 60 | activeresource (= 3.1.0) 61 | activesupport (= 3.1.0) 62 | bundler (~> 1.0) 63 | railties (= 3.1.0) 64 | railties (3.1.0) 65 | actionpack (= 3.1.0) 66 | activesupport (= 3.1.0) 67 | rack-ssl (~> 1.3.2) 68 | rake (>= 0.8.7) 69 | rdoc (~> 3.4) 70 | thor (~> 0.14.6) 71 | rake (0.9.2) 72 | rdoc (3.9.4) 73 | sprockets (2.0.0) 74 | hike (~> 1.2) 75 | rack (~> 1.0) 76 | tilt (!= 1.3.0, ~> 1.1) 77 | sqlite3 (1.3.4) 78 | thor (0.14.6) 79 | tilt (1.3.3) 80 | treetop (1.4.10) 81 | polyglot 82 | polyglot (>= 0.3.1) 83 | tzinfo (0.3.29) 84 | 85 | PLATFORMS 86 | ruby 87 | 88 | DEPENDENCIES 89 | mysql2 90 | rails (= 3.1.0) 91 | sqlite3 92 | -------------------------------------------------------------------------------- /examples/ar-3.x/README.md: -------------------------------------------------------------------------------- 1 | Replicate - ActiveRecord 3.1 Example Environment 2 | ================================================ 3 | 4 | This is a skeleton Rails environment that includes some basic seed data for 5 | experimenting with replicate. First, run `rake setup` to install gem 6 | dependencies and copy over the sample database: 7 | 8 | $ rake setup 9 | 10 | The sample database is copied into the development environment. The schema is 11 | straightforward. See `db/schema.rb` for the gist. 12 | 13 | Dumping a Country object will bring in all associated cities and languages: 14 | 15 | $ replicate -r ./config/environment -d "Country.first" > country.dump 16 | ==> dumped 10 total objects: 17 | City 4 18 | Country 1 19 | Language 5 20 | 21 | Now that you have a dump file at country.dump, you can load it into your test 22 | database: 23 | 24 | $ RAILS_ENV=test replicate -r ./config/environment -l < country.dump 25 | ==> loaded 10 total objects: 26 | City 4 27 | Country 1 28 | Language 5 29 | 30 | Dump everything: 31 | 32 | $ replicate -r ./config/environment -d "Country.all" > countries.dump 33 | ==> dumped 5302 total objects: 34 | City 4079 35 | Country 239 36 | Language 984 37 | -------------------------------------------------------------------------------- /examples/ar-3.x/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Ar3X::Application.load_tasks 8 | 9 | task :setup do 10 | sh 'bundle install' 11 | cp 'db/world.sqlite3', 'db/development.sqlite3' 12 | end 13 | -------------------------------------------------------------------------------- /examples/ar-3.x/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/replicate/008f53240893c18cf16ee5d6edbf888501dfd165/examples/ar-3.x/app/models/.gitkeep -------------------------------------------------------------------------------- /examples/ar-3.x/app/models/city.rb: -------------------------------------------------------------------------------- 1 | class City < ActiveRecord::Base 2 | belongs_to :country 3 | end 4 | -------------------------------------------------------------------------------- /examples/ar-3.x/app/models/country.rb: -------------------------------------------------------------------------------- 1 | class Country < ActiveRecord::Base 2 | has_many :cities 3 | has_many :languages 4 | 5 | replicate_associations :cities, :languages 6 | end 7 | -------------------------------------------------------------------------------- /examples/ar-3.x/app/models/language.rb: -------------------------------------------------------------------------------- 1 | class Language < ActiveRecord::Base 2 | belongs_to :country 3 | end 4 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | module Ar3X 6 | class Application < Rails::Application 7 | # Settings in config/environments/* take precedence over those specified here. 8 | # Application configuration should go into files in config/initializers 9 | # -- all .rb files in that directory are automatically loaded. 10 | 11 | # Custom directories with classes and modules you want to be autoloadable. 12 | # config.autoload_paths += %W(#{config.root}/extras) 13 | 14 | # Only load the plugins named here, in the order given (default is alphabetical). 15 | # :all can be used as a placeholder for all plugins not explicitly named. 16 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 17 | 18 | # Activate observers that should always be running. 19 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 20 | 21 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 22 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 23 | # config.time_zone = 'Central Time (US & Canada)' 24 | 25 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 26 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 27 | # config.i18n.default_locale = :de 28 | 29 | # Configure the default encoding used in templates for Ruby 1.9. 30 | config.encoding = "utf-8" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | require 'replicate' 5 | 6 | # Initialize the rails application 7 | Ar3X::Application.initialize! 8 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Ar3X::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | config.assets.compress = false 27 | 28 | # Expands the lines which load the assets 29 | config.assets.debug = true 30 | end 31 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Ar3X::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Use a different logger for distributed setups 37 | # config.logger = SyslogLogger.new 38 | 39 | # Use a different cache store in production 40 | # config.cache_store = :mem_cache_store 41 | 42 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 43 | # config.action_controller.asset_host = "http://assets.example.com" 44 | 45 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 46 | # config.assets.precompile += %w( search.js ) 47 | 48 | # Disable delivery errors, bad email addresses will be ignored 49 | # config.action_mailer.raise_delivery_errors = false 50 | 51 | # Enable threaded mode 52 | # config.threadsafe! 53 | 54 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 55 | # the I18n.default_locale when a translation can not be found) 56 | config.i18n.fallbacks = true 57 | 58 | # Send deprecation notices to registered listeners 59 | config.active_support.deprecation = :notify 60 | end 61 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Ar3X::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Use SQL instead of Active Record's schema dumper when creating the test database. 33 | # This is necessary if your schema can't be completely dumped by the schema dumper, 34 | # like if you have constraints or database-specific column types 35 | # config.active_record.schema_format = :sql 36 | 37 | # Print deprecation notices to the stderr 38 | config.active_support.deprecation = :stderr 39 | 40 | # Allow pass debug_assets=true as a query parameter to load pages with unpackaged assets 41 | config.assets.allow_debugging = true 42 | end 43 | -------------------------------------------------------------------------------- /examples/ar-3.x/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/migrate/20110909181231_create_world_schema.rb: -------------------------------------------------------------------------------- 1 | class CreateWorldSchema < ActiveRecord::Migration 2 | def up 3 | rename_table 'City', 'cities' 4 | rename_column 'cities', "Name", 'name' 5 | rename_column 'cities', "CountryCode", 'country_code' 6 | rename_column 'cities', "District", 'district' 7 | rename_column 'cities', "Population", 'population' 8 | 9 | rename_table 'Country', 'countries' 10 | rename_column 'countries', "Name", 'name' 11 | rename_column 'countries', "Continent", 'continent' 12 | rename_column 'countries', "Region", 'region' 13 | rename_column 'countries', "SurfaceArea", 'surface_area' 14 | rename_column 'countries', "IndepYear", 'year_of_independence' 15 | rename_column 'countries', "Population", 'population' 16 | rename_column 'countries', "LifeExpectancy", 'life_expectancy' 17 | rename_column 'countries', "GNP", 'gross_national_product' 18 | rename_column 'countries', "GNPOld", 'gnp_old' 19 | rename_column 'countries', "LocalName", 'local_name' 20 | rename_column 'countries', "GovernmentForm", 'government_form' 21 | rename_column 'countries', "HeadOfState", 'head_of_state' 22 | rename_column 'countries', "Capital", 'capital' 23 | rename_column 'countries', "Code2", 'code2' 24 | 25 | rename_table 'CountryLanguage', 'countries_languages' 26 | rename_column 'countries_languages', "CountryCode", 'country_code' 27 | rename_column 'countries_languages', "Language", 'language' 28 | rename_column 'countries_languages', "IsOfficial", 'official' 29 | rename_column 'countries_languages', "Percentage", 'percentage' 30 | end 31 | 32 | def down 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/migrate/20110909184144_railsize_foreign_key_columns.rb: -------------------------------------------------------------------------------- 1 | class City < ActiveRecord::Base 2 | end 3 | class Country < ActiveRecord::Base 4 | set_primary_key 'code' 5 | end 6 | 7 | class RailsizeForeignKeyColumns < ActiveRecord::Migration 8 | def up 9 | rename_column 'cities', 'ID', 'id' 10 | rename_column 'countries', 'Code', 'code' 11 | add_column 'countries', 'id', :integer 12 | add_column 'cities', 'country_id', :integer 13 | 14 | count = 0 15 | Country.all.each do |record| 16 | count += 1 17 | record.update_attribute :id, count 18 | end 19 | end 20 | 21 | def down 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/migrate/20110909190458_railsize_countries_language_country_id.rb: -------------------------------------------------------------------------------- 1 | class Language < ActiveRecord::Base 2 | set_primary_key :country_code 3 | end 4 | class Country < ActiveRecord::Base 5 | end 6 | 7 | class RailsizeCountriesLanguageCountryId < ActiveRecord::Migration 8 | def up 9 | rename_table 'countries_languages', 'languages' 10 | add_column 'languages', 'id', :integer 11 | add_column 'languages', 'country_id', :integer 12 | 13 | execute "ALTER TABLE languages DROP PRIMARY KEY" 14 | 15 | puts "languages = #{Language.count}" 16 | 17 | count = 0 18 | Language.all.each do |record| 19 | count += 1 20 | execute " 21 | UPDATE languages 22 | SET id = #{count} 23 | WHERE country_code = '#{record.country_code}' 24 | AND language = '#{record.language}' 25 | " 26 | end 27 | execute "ALTER TABLE languages ADD PRIMARY KEY (id)" 28 | execute "ALTER TABLE languages MODIFY id int(11) NOT NULL AUTO_INCREMENT" 29 | end 30 | 31 | def down 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/migrate/20110909194842_set_country_ids.rb: -------------------------------------------------------------------------------- 1 | class Country < ActiveRecord::Base 2 | end 3 | 4 | class SetCountryIds < ActiveRecord::Migration 5 | def up 6 | count = 0 7 | Country.all.each do |record| 8 | count += 1 9 | execute "UPDATE countries SET id = #{count} WHERE code = '#{record.code}'" 10 | end 11 | 12 | execute "UPDATE cities SET country_id = (SELECT countries.id FROM countries WHERE countries.code = cities.country_code)" 13 | execute "UPDATE languages SET country_id = (SELECT countries.id FROM countries WHERE countries.code = languages.country_code)" 14 | 15 | execute "ALTER TABLE countries DROP PRIMARY KEY" 16 | execute "ALTER TABLE countries ADD PRIMARY KEY (id)" 17 | execute "ALTER TABLE countries MODIFY id int(11) NOT NULL AUTO_INCREMENT" 18 | end 19 | 20 | def down 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20110909194842) do 15 | create_table "cities", :force => true do |t| 16 | t.string "name", :limit => 35, :default => "", :null => false 17 | t.string "country_code", :limit => 3, :default => "", :null => false 18 | t.string "district", :limit => 20, :default => "", :null => false 19 | t.integer "population", :default => 0, :null => false 20 | t.integer "country_id" 21 | end 22 | 23 | create_table "countries", :force => true do |t| 24 | t.string "code", :limit => 3, :default => "", :null => false 25 | t.string "name", :limit => 52, :default => "", :null => false 26 | t.string "continent", :limit => 0, :default => "Asia", :null => false 27 | t.string "region", :limit => 26, :default => "", :null => false 28 | t.float "surface_area", :limit => 10, :default => 0.0, :null => false 29 | t.integer "year_of_independence", :limit => 2 30 | t.integer "population", :default => 0, :null => false 31 | t.float "life_expectancy", :limit => 3 32 | t.float "gross_national_product", :limit => 10 33 | t.float "gnp_old", :limit => 10 34 | t.string "local_name", :limit => 45, :default => "", :null => false 35 | t.string "government_form", :limit => 45, :default => "", :null => false 36 | t.string "head_of_state", :limit => 60 37 | t.integer "capital" 38 | t.string "code2", :limit => 2, :default => "", :null => false 39 | end 40 | 41 | create_table "languages", :force => true do |t| 42 | t.integer "country_id" 43 | t.string "country_code", :limit => 3, :default => "", :null => false 44 | t.string "language", :limit => 30, :default => "", :null => false 45 | t.string "official", :limit => 0, :default => "F", :null => false 46 | t.float "percentage", :limit => 4, :default => 0.0, :null => false 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /examples/ar-3.x/db/world.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/replicate/008f53240893c18cf16ee5d6edbf888501dfd165/examples/ar-3.x/db/world.sqlite3 -------------------------------------------------------------------------------- /examples/ar-3.x/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/replicate/008f53240893c18cf16ee5d6edbf888501dfd165/examples/ar-3.x/log/.gitkeep -------------------------------------------------------------------------------- /examples/ar-3.x/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /lib/replicate.rb: -------------------------------------------------------------------------------- 1 | module Replicate 2 | autoload :Emitter, 'replicate/emitter' 3 | autoload :Dumper, 'replicate/dumper' 4 | autoload :Loader, 'replicate/loader' 5 | autoload :Object, 'replicate/object' 6 | autoload :Status, 'replicate/status' 7 | autoload :AR, 'replicate/active_record' 8 | 9 | # Determine if this is a production looking environment. Used in bin/replicate 10 | # to safeguard against loading in production. 11 | def self.production_environment? 12 | if defined?(Rails) && Rails.respond_to?(:env) 13 | Rails.env.to_s == 'production' 14 | elsif defined?(RAILS_ENV) 15 | RAILS_ENV == 'production' 16 | elsif ENV['RAILS_ENV'] 17 | ENV['RAILS_ENV'] == 'production' 18 | elsif ENV['RACK_ENV'] 19 | ENV['RACK_ENV'] == 'production' 20 | end 21 | end 22 | 23 | AR if defined?(::ActiveRecord::Base) 24 | end 25 | -------------------------------------------------------------------------------- /lib/replicate/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_record/version' 3 | 4 | module Replicate 5 | # ActiveRecord::Base instance methods used to dump replicant objects for the 6 | # record and all 1:1 associations. This module implements the replicant_id 7 | # and dump_replicant methods using AR's reflection API to determine 8 | # relationships with other objects. 9 | module AR 10 | # Mixin for the ActiveRecord instance. 11 | module InstanceMethods 12 | # Replicate::Dumper calls this method on objects to trigger dumping a 13 | # replicant object tuple. The default implementation dumps all belongs_to 14 | # associations, then self, then all has_one associations, then any 15 | # has_many or has_and_belongs_to_many associations declared with the 16 | # replicate_associations macro. 17 | # 18 | # dumper - Dumper object whose #write method must be called with the 19 | # type, id, and attributes hash. 20 | # 21 | # Returns nothing. 22 | def dump_replicant(dumper, opts={}) 23 | @replicate_opts = opts 24 | @replicate_opts[:associations] ||= [] 25 | @replicate_opts[:omit] ||= [] 26 | dump_all_association_replicants dumper, :belongs_to 27 | dumper.write self.class.to_s, id, replicant_attributes, self 28 | dump_all_association_replicants dumper, :has_one 29 | included_associations.each do |association| 30 | dump_association_replicants dumper, association 31 | end 32 | end 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 | 44 | # Attributes hash used to persist this object. This consists of simply 45 | # typed values (no complex types or objects) with the exception of special 46 | # foreign key values. When an attribute value is [:id, "SomeClass:1234"], 47 | # the loader will handle translating the id value to the local system's 48 | # version of the same object. 49 | def replicant_attributes 50 | attributes = self.attributes.dup 51 | 52 | omitted_attributes.each { |omit| attributes.delete(omit.to_s) } 53 | self.class.reflect_on_all_associations(:belongs_to).each do |reflection| 54 | next if omitted_attributes.include?(reflection.name) 55 | if info = replicate_reflection_info(reflection) 56 | if replicant_id = info[:replicant_id] 57 | foreign_key = info[:foreign_key].to_s 58 | attributes[foreign_key] = [:id, *replicant_id] 59 | end 60 | end 61 | end 62 | 63 | attributes 64 | end 65 | 66 | # Retrieve information on a reflection's associated class and various 67 | # keys. 68 | # 69 | # Returns an info hash with these keys: 70 | # :class - The class object the association points to. 71 | # :primary_key - The string primary key column name. 72 | # :foreign_key - The string foreign key column name. 73 | # :replicant_id - The [classname, id] tuple identifying the record. 74 | # 75 | # Returns nil when the reflection can not be linked to a model. 76 | def replicate_reflection_info(reflection) 77 | options = reflection.options 78 | if options[:polymorphic] 79 | reference_class = 80 | if attributes[options[:foreign_type]] 81 | attributes[options[:foreign_type]] 82 | else 83 | attributes[reflection.foreign_type] 84 | end 85 | return if reference_class.nil? 86 | 87 | klass = reference_class.constantize 88 | primary_key = klass.primary_key 89 | foreign_key = "#{reflection.name}_id" 90 | else 91 | klass = reflection.klass 92 | primary_key = (options[:primary_key] || klass.primary_key).to_s 93 | foreign_key = (options[:foreign_key] || "#{reflection.name}_id").to_s 94 | end 95 | 96 | info = { 97 | :class => klass, 98 | :primary_key => primary_key, 99 | :foreign_key => foreign_key 100 | } 101 | 102 | if primary_key == klass.primary_key 103 | if id = attributes[foreign_key] 104 | info[:replicant_id] = [klass.to_s, id] 105 | else 106 | # nil value in association reference 107 | end 108 | else 109 | # association uses non-primary-key foreign key. no special key 110 | # conversion needed. 111 | end 112 | 113 | info 114 | end 115 | 116 | # The replicant id is a two tuple containing the class and object id. This 117 | # is used by Replicant::Dumper to determine if the object has already been 118 | # dumped or not. 119 | def replicant_id 120 | [self.class.name, id] 121 | end 122 | 123 | # Dump all associations of a given type. 124 | # 125 | # dumper - The Dumper object used to dump additional objects. 126 | # association_type - :has_one, :belongs_to, :has_many 127 | # 128 | # Returns nothing. 129 | def dump_all_association_replicants(dumper, association_type) 130 | self.class.reflect_on_all_associations(association_type).each do |reflection| 131 | next if omitted_attributes.include?(reflection.name) 132 | 133 | # bail when this object has already been dumped 134 | next if (info = replicate_reflection_info(reflection)) && 135 | (replicant_id = info[:replicant_id]) && 136 | dumper.dumped?(replicant_id) 137 | 138 | next if (dependent = __send__(reflection.name)).nil? 139 | 140 | case dependent 141 | when ActiveRecord::Base, Array 142 | dumper.dump(dependent) 143 | 144 | # clear reference to allow GC 145 | if respond_to?(:association) 146 | association(reflection.name).reset 147 | elsif respond_to?(:association_instance_set, true) 148 | association_instance_set(reflection.name, nil) 149 | end 150 | else 151 | warn "warn: #{self.class}##{reflection.name} #{association_type} association " \ 152 | "unexpectedly returned a #{dependent.class}. skipping." 153 | end 154 | end 155 | end 156 | 157 | # Dump objects associated with an AR object through an association name. 158 | # 159 | # object - AR object instance. 160 | # association - Name of the association whose objects should be dumped. 161 | # 162 | # Returns nothing. 163 | def dump_association_replicants(dumper, association) 164 | if reflection = self.class.reflect_on_association(association) 165 | objects = __send__(reflection.name) 166 | dumper.dump(objects) 167 | if reflection.macro == :has_and_belongs_to_many 168 | dump_has_and_belongs_to_many_replicant(dumper, reflection) 169 | end 170 | __send__(reflection.name).reset # clear to allow GC 171 | else 172 | warn "error: #{self.class}##{association} is invalid" 173 | end 174 | end 175 | 176 | # Dump the special Habtm object used to establish many-to-many 177 | # relationships between objects that have already been dumped. Note that 178 | # this object and all objects referenced must have already been dumped 179 | # before calling this method. 180 | def dump_has_and_belongs_to_many_replicant(dumper, reflection) 181 | dumper.dump Habtm.new(self, reflection) 182 | end 183 | end 184 | 185 | # Mixin for the ActiveRecord class. 186 | module ClassMethods 187 | # Set and retrieve list of association names that should be dumped when 188 | # objects of this class are dumped. This method may be called multiple 189 | # times to add associations. 190 | def replicate_associations(*names) 191 | self.replicate_associations += names if names.any? 192 | @replicate_associations || superclass.replicate_associations 193 | end 194 | 195 | # Set the list of association names to dump to the specific set of values. 196 | def replicate_associations=(names) 197 | @replicate_associations = names.uniq.map { |name| name.to_sym } 198 | end 199 | 200 | # Compound key used during load to locate existing objects for update. 201 | # When no natural key is defined, objects are created new. 202 | # 203 | # attribute_names - Macro style setter. 204 | def replicate_natural_key(*attribute_names) 205 | self.replicate_natural_key = attribute_names if attribute_names.any? 206 | @replicate_natural_key || superclass.replicate_natural_key 207 | end 208 | 209 | # Set the compound key used to locate existing objects for update when 210 | # loading. When not set, loading will always create new records. 211 | # 212 | # attribute_names - Array of attribute name symbols 213 | def replicate_natural_key=(attribute_names) 214 | @replicate_natural_key = attribute_names 215 | end 216 | 217 | # Set or retrieve whether replicated object should keep its original id. 218 | # When not set, replicated objects will be created with new id. 219 | def replicate_id(boolean=nil) 220 | self.replicate_id = boolean unless boolean.nil? 221 | @replicate_id.nil? ? superclass.replicate_id : @replicate_id 222 | end 223 | 224 | # Set flag for replicating original id. 225 | def replicate_id=(boolean) 226 | self.replicate_natural_key = [:id] if boolean 227 | @replicate_id = boolean 228 | end 229 | 230 | # Set which, if any, attributes should not be dumped. Also works for 231 | # associations. 232 | # 233 | # attribute_names - Macro style setter. 234 | def replicate_omit_attributes(*attribute_names) 235 | self.replicate_omit_attributes = attribute_names if attribute_names.any? 236 | @replicate_omit_attributes || superclass.replicate_omit_attributes 237 | end 238 | 239 | # Set which, if any, attributes should not be dumped. Also works for 240 | # associations. 241 | # 242 | # attribute_names - Array of attribute name symbols 243 | def replicate_omit_attributes=(attribute_names) 244 | @replicate_omit_attributes = attribute_names 245 | end 246 | 247 | # Load an individual record into the database. If the models defines a 248 | # replicate_natural_key then an existing record will be updated if found 249 | # instead of a new record being created. 250 | # 251 | # type - Model class name as a String. 252 | # id - Primary key id of the record on the dump system. This must be 253 | # translated to the local system and stored in the keymap. 254 | # attrs - Hash of attributes to set on the new record. 255 | # 256 | # Returns the ActiveRecord object instance for the new record. 257 | def load_replicant(type, id, attributes) 258 | instance = replicate_find_existing_record(attributes) || new 259 | create_or_update_replicant instance, attributes 260 | end 261 | 262 | # Locate an existing record using the replicate_natural_key attribute 263 | # values. 264 | # 265 | # Returns the existing record if found, nil otherwise. 266 | def replicate_find_existing_record(attributes) 267 | return if replicate_natural_key.empty? 268 | conditions = {} 269 | replicate_natural_key.each do |attribute_name| 270 | conditions[attribute_name] = attributes[attribute_name.to_s] 271 | end 272 | if ::ActiveRecord::VERSION::MAJOR >= 4 273 | where(conditions).first 274 | else 275 | find(:first, :conditions => conditions) 276 | end 277 | end 278 | 279 | # Update an AR object's attributes and persist to the database without 280 | # running validations or callbacks. 281 | # 282 | # Returns the [id, object] tuple for the newly replicated objected. 283 | def create_or_update_replicant(instance, attributes) 284 | # write replicated attributes to the instance 285 | attributes.each do |key, value| 286 | next if skip_attribute?(instance.class, key) 287 | instance.send :write_attribute, key, value 288 | end 289 | 290 | # save the instance bypassing all callbacks and validations 291 | replicate_disable_callbacks instance 292 | if ::ActiveRecord::VERSION::MAJOR >= 3 293 | instance.save :validate => false 294 | else 295 | instance.save false 296 | end 297 | 298 | [instance.id, instance] 299 | end 300 | 301 | def skip_attribute?(klass, key) # :nodoc: 302 | (key == primary_key && !replicate_id) || klass.replicate_omit_attributes.include?(key.to_sym) 303 | end 304 | 305 | # Disable all callbacks on an ActiveRecord::Base instance. Only the 306 | # instance is effected. There is no way to re-enable callbacks once 307 | # they've been disabled on an object. 308 | def replicate_disable_callbacks(instance) 309 | if ::ActiveRecord::VERSION::MAJOR >= 3 310 | # AR 3.1.x, 3.2.x 311 | def instance.run_callbacks(*args); yield if block_given?; end 312 | 313 | # AR 3.0.x 314 | def instance._run_save_callbacks(*args); yield if block_given?; end 315 | def instance._run_create_callbacks(*args); yield if block_given?; end 316 | def instance._run_update_callbacks(*args); yield if block_given?; end 317 | def instance._run_commit_callbacks(*args); yield if block_given?; end 318 | else 319 | # AR 2.x 320 | def instance.callback(*args) 321 | end 322 | def instance.record_timestamps 323 | false 324 | end 325 | end 326 | end 327 | 328 | end 329 | 330 | # Special object used to dump the list of associated ids for a 331 | # has_and_belongs_to_many association. The object includes attributes for 332 | # locating the source object and writing the list of ids to the appropriate 333 | # association method. 334 | class Habtm 335 | def initialize(object, reflection) 336 | @object = object 337 | @reflection = reflection 338 | end 339 | 340 | def id 341 | end 342 | 343 | def attributes 344 | ids = @object.__send__("#{@reflection.name.to_s.singularize}_ids") 345 | { 346 | 'id' => [:id, @object.class.to_s, @object.id], 347 | 'class' => @object.class.to_s, 348 | 'ref_class' => @reflection.klass.to_s, 349 | 'ref_name' => @reflection.name.to_s, 350 | 'collection' => [:id, @reflection.klass.to_s, ids] 351 | } 352 | end 353 | 354 | def dump_replicant(dumper, opts={}) 355 | type = self.class.name 356 | id = "#{@object.class.to_s}:#{@reflection.name}:#{@object.id}" 357 | dumper.write type, id, attributes, self 358 | end 359 | 360 | def self.load_replicant(type, id, attrs) 361 | object = attrs['class'].constantize.find(attrs['id']) 362 | ids = attrs['collection'] 363 | object.__send__("#{attrs['ref_name'].to_s.singularize}_ids=", ids) 364 | [id, new(object, nil)] 365 | end 366 | end 367 | 368 | # Load active record and install the extension methods. 369 | ::ActiveRecord::Base.send :include, InstanceMethods 370 | ::ActiveRecord::Base.send :extend, ClassMethods 371 | ::ActiveRecord::Base.replicate_associations = [] 372 | ::ActiveRecord::Base.replicate_natural_key = [] 373 | ::ActiveRecord::Base.replicate_omit_attributes = [] 374 | ::ActiveRecord::Base.replicate_id = false 375 | end 376 | end 377 | -------------------------------------------------------------------------------- /lib/replicate/dumper.rb: -------------------------------------------------------------------------------- 1 | module Replicate 2 | # Dump replicants in a streaming fashion. 3 | # 4 | # The Dumper takes objects and generates one or more replicant objects. A 5 | # replicant has the form [type, id, attributes] and describes exactly one 6 | # addressable record in a datastore. The type and id identify the model 7 | # class name and model primary key id. The attributes Hash is a set of attribute 8 | # name to primitively typed object value mappings. 9 | # 10 | # Example dump session: 11 | # 12 | # >> Replicate::Dumper.new do |dumper| 13 | # >> dumper.marshal_to $stdout 14 | # >> dumper.log_to $stderr 15 | # >> dumper.dump User.find(1234) 16 | # >> end 17 | # 18 | class Dumper < Emitter 19 | # Create a new Dumper. 20 | # 21 | # io - IO object to write marshalled replicant objects to. 22 | # block - Dump context block. If given, the end of the block's execution 23 | # is assumed to be the end of the dump stream. 24 | def initialize(io=nil) 25 | @memo = Hash.new { |hash,k| hash[k] = {} } 26 | super() do 27 | marshal_to io if io 28 | yield self if block_given? 29 | end 30 | end 31 | 32 | # Register a filter to write marshalled data to the given IO object. 33 | def marshal_to(io) 34 | listen { |type, id, attrs, obj| Marshal.dump([type, id, attrs], io) } 35 | end 36 | 37 | # Register a filter to write status information to the given stream. By 38 | # default, a single line is used to report object counts while the dump is 39 | # in progress; dump counts for each class are written when complete. The 40 | # verbose and quiet options can be used to increase or decrease 41 | # verbosity. 42 | # 43 | # out - An IO object to write to, like stderr. 44 | # verbose - Whether verbose output should be enabled. 45 | # quiet - Whether quiet output should be enabled. 46 | # 47 | # Returns the Replicate::Status object. 48 | def log_to(out=$stderr, verbose=false, quiet=false) 49 | use Replicate::Status, 'dump', out, verbose, quiet 50 | end 51 | 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. 56 | def load_script(path) 57 | dumper = self 58 | object = ::Object.new 59 | meta = (class< count } where count is the number of 124 | # objects dumped with a class of class_name. 125 | def stats 126 | stats = {} 127 | @memo.each { |class_name, items| stats[class_name] = items.size } 128 | stats 129 | end 130 | 131 | protected 132 | def find_file(path) 133 | path = "#{path}.rb" unless path =~ /\.rb$/ 134 | return path if File.exists? path 135 | $LOAD_PATH.each do |prefix| 136 | full_path = File.expand_path(path, prefix) 137 | return full_path if File.exists? full_path 138 | end 139 | false 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/replicate/emitter.rb: -------------------------------------------------------------------------------- 1 | module Replicate 2 | # Base class for Dumper / Loader classes. Manages a list of callback listeners 3 | # and dispatches to each when #emit is called. 4 | class Emitter 5 | # Yields self to the block and calls #complete when block is finished. 6 | def initialize 7 | @listeners = [] 8 | if block_given? 9 | yield self 10 | complete 11 | end 12 | end 13 | 14 | # Register a listener to be called for each loaded object with the 15 | # type, id, attributes, object structure. Listeners are executed in the 16 | # reverse order of which they were registered. Listeners registered later 17 | # modify the view of listeners registered earlier. 18 | # 19 | # p - An optional Proc object. Must respond to call. 20 | # block - An optional block. 21 | # 22 | # Returns nothing. 23 | def listen(p=nil, &block) 24 | @listeners.unshift p if p 25 | @listeners.unshift block if block 26 | end 27 | 28 | # Sugar for creating a listener with an object instance. Instances of the 29 | # class must respond to call(type, id, attrs, object). 30 | # 31 | # klass - The class to create. Must respond to new. 32 | # args - Arguments to pass to new in addition to self. 33 | # 34 | # Returns the object created. 35 | def use(klass, *args, &block) 36 | instance = klass.new(self, *args, &block) 37 | listen instance 38 | instance 39 | end 40 | 41 | # Emit an object event to each listener. 42 | # 43 | # Returns the object. 44 | def emit(type, id, attributes, object) 45 | @listeners.each { |p| p.call(type, id, attributes, object) } 46 | object 47 | end 48 | 49 | # Notify all listeners that processing is complete. 50 | def complete 51 | @listeners.each { |p| p.complete if p.respond_to?(:complete) } 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/replicate/loader.rb: -------------------------------------------------------------------------------- 1 | module Replicate 2 | # Load replicants in a streaming fashion. 3 | # 4 | # The Loader reads [type, id, attributes] replicant tuples and creates 5 | # objects in the current environment. 6 | # 7 | # Objects are expected to arrive in order such that a record referenced via 8 | # foreign key always precedes the referencing record. The Loader maintains a 9 | # mapping of primary keys from the dump system to the current environment. 10 | # This mapping is used to properly establish new foreign key values on all 11 | # records inserted. 12 | class Loader < Emitter 13 | 14 | # Stats hash. 15 | attr_reader :stats 16 | 17 | def initialize 18 | @keymap = Hash.new { |hash,k| hash[k] = {} } 19 | @stats = Hash.new { |hash,k| hash[k] = 0 } 20 | super 21 | end 22 | 23 | # Register a filter to write status information to the given stream. By 24 | # default, a single line is used to report object counts while the dump is 25 | # in progress; dump counts for each class are written when complete. The 26 | # verbose and quiet options can be used to increase or decrease 27 | # verbository. 28 | # 29 | # out - An IO object to write to, like stderr. 30 | # verbose - Whether verbose output should be enabled. 31 | # quiet - Whether quiet output should be enabled. 32 | # 33 | # Returns the Replicate::Status object. 34 | def log_to(out=$stderr, verbose=false, quiet=false) 35 | use Replicate::Status, 'load', out, verbose, quiet 36 | end 37 | 38 | # Feed a single replicant tuple into the loader. 39 | # 40 | # type - The class to create. Must respond to load_replicant. 41 | # id - The remote system's id for this object. 42 | # attrs - Hash of primitively typed objects. 43 | # 44 | # Returns the need object resulting from the load operation. 45 | def feed(type, id, attrs) 46 | type = type.to_s 47 | object = load(type, id, attrs) 48 | @stats[type] += 1 49 | emit type, id, attrs, object 50 | end 51 | 52 | # Read multiple [type, id, attrs] replicant tuples from io and call the 53 | # feed method to load and filter the object. 54 | def read(io) 55 | begin 56 | while object = Marshal.load(io) 57 | type, id, attrs = object 58 | feed type, id, attrs 59 | end 60 | rescue EOFError 61 | end 62 | end 63 | 64 | # Load an individual replicant into the underlying datastore. 65 | # 66 | # type - Model class name as a String. 67 | # id - Primary key id of the record on the dump system. This must be 68 | # translated to the local system and stored in the keymap. 69 | # attrs - Hash of attributes to set on the new record. 70 | # 71 | # Returns the new object instance. 72 | def load(type, id, attributes) 73 | model_class = constantize(type) 74 | translate_ids type, id, attributes 75 | begin 76 | new_id, instance = model_class.load_replicant(type, id, attributes) 77 | rescue => boom 78 | warn "error: loading #{type} #{id} #{boom.class} #{boom}" 79 | raise 80 | end 81 | register_id instance, type, id, new_id 82 | instance 83 | end 84 | 85 | # Translate remote system id references in the attributes hash to their 86 | # local system id values. The attributes hash may include special id 87 | # values like this: 88 | # { 'title' => 'hello there', 89 | # 'repository_id' => [:id, 'Repository', 1234], 90 | # 'label_ids' => [:id, 'Label', [333, 444, 555, 666, ...]] 91 | # ... } 92 | # These values are translated to local system ids. All object 93 | # references must be loaded prior to the referencing object. 94 | def translate_ids(type, id, attributes) 95 | attributes.each do |key, value| 96 | next unless value.is_a?(Array) && value[0] == :id 97 | referenced_type, value = value[1].to_s, value[2] 98 | local_ids = 99 | Array(value).map do |remote_id| 100 | if local_id = @keymap[referenced_type][remote_id] 101 | local_id 102 | else 103 | warn "warn: #{referenced_type}(#{remote_id}) not in keymap, " + 104 | "referenced by #{type}(#{id})##{key}" 105 | end 106 | end 107 | if value.is_a?(Array) 108 | attributes[key] = local_ids 109 | else 110 | attributes[key] = local_ids[0] 111 | end 112 | end 113 | end 114 | 115 | # Register an id in the keymap. Every object loaded must be stored here so 116 | # that key references can be resolved. 117 | def register_id(object, type, remote_id, local_id) 118 | @keymap[type.to_s][remote_id] = local_id 119 | c = object.class 120 | while !['Object', 'ActiveRecord::Base'].include?(c.name) 121 | @keymap[c.name][remote_id] = local_id 122 | c = c.superclass 123 | end 124 | end 125 | 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 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/replicate/object.rb: -------------------------------------------------------------------------------- 1 | module Replicate 2 | # Simple OpenStruct style object that supports the dump and load protocols. 3 | # Useful in tests and also when you want to dump an object that doesn't 4 | # implement the dump and load methods. 5 | # 6 | # >> object = Replicate::Object.new :name => 'Joe', :age => 24 7 | # >> object.age 8 | # >> 24 9 | # >> object.attributes 10 | # { 'name' => 'Joe', 'age' => 24 } 11 | # 12 | class Object 13 | attr_accessor :id 14 | attr_accessor :attributes 15 | 16 | def initialize(id=nil, attributes={}) 17 | attributes, id = id, nil if id.is_a?(Hash) 18 | @id = id || self.class.generate_id 19 | self.attributes = attributes 20 | end 21 | 22 | def attributes=(hash) 23 | @attributes = {} 24 | hash.each { |key, value| write_attribute key, value } 25 | end 26 | 27 | def [](key) 28 | @attributes[key.to_s] 29 | end 30 | 31 | def []=(key, value) 32 | @attributes[key.to_s] = value 33 | end 34 | 35 | def write_attribute(key, value) 36 | meta = (class< #{@prefix}ed #{@count} total objects: " 39 | width = 0 40 | stats.keys.each do |key| 41 | class_name = format_class_name(key) 42 | stats[class_name] = stats.delete(key) 43 | width = class_name.size if class_name.size > width 44 | end 45 | stats.to_a.sort_by { |k,n| k }.each do |class_name, count| 46 | @out.write "%-#{width + 1}s %5d\n" % [class_name, count] 47 | end 48 | end 49 | 50 | def format_class_name(class_name) 51 | class_name.sub(/Replicate::/, '') 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /replicate.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.specification_version = 2 if s.respond_to? :specification_version= 3 | 4 | s.name = 'replicate' 5 | s.version = '1.5.1' 6 | s.date = '2011-10-19' 7 | s.homepage = "http://github.com/rtomayko/replicate" 8 | s.authors = ["Ryan Tomayko"] 9 | s.email = "ryan@github.com" 10 | 11 | s.description = "Dump and load relational objects between Ruby environments." 12 | s.summary = s.description 13 | 14 | s.files = %w[ 15 | COPYING 16 | HACKING 17 | README.md 18 | Rakefile 19 | bin/replicate 20 | lib/replicate.rb 21 | lib/replicate/active_record.rb 22 | lib/replicate/dumper.rb 23 | lib/replicate/emitter.rb 24 | lib/replicate/loader.rb 25 | lib/replicate/object.rb 26 | lib/replicate/status.rb 27 | test/active_record_test.rb 28 | test/dumper_test.rb 29 | test/dumpscript.rb 30 | test/linked_dumpscript.rb 31 | test/loader_test.rb 32 | test/replicate_test.rb 33 | ] 34 | 35 | s.executables = ['replicate'] 36 | s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/} 37 | s.add_development_dependency 'activerecord', '~> 3.1' 38 | s.add_development_dependency 'sqlite3' 39 | s.add_development_dependency 'test_after_commit' 40 | 41 | s.require_paths = %w[lib] 42 | end 43 | -------------------------------------------------------------------------------- /test/active_record_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'stringio' 3 | 4 | # require a specific AR version. 5 | version = ENV['AR_VERSION'] 6 | gem 'activerecord', "~> #{version}" if version 7 | require 'active_record' 8 | require 'active_record/version' 9 | version = ActiveRecord::VERSION::STRING 10 | warn "Using activerecord #{version}" 11 | 12 | # replicate must be loaded after AR 13 | require 'replicate' 14 | 15 | # create the sqlite db on disk 16 | dbfile = File.expand_path('../db', __FILE__) 17 | File.unlink dbfile if File.exist?(dbfile) 18 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => dbfile) 19 | require 'test_after_commit' 20 | 21 | # load schema 22 | ActiveRecord::Migration.verbose = false 23 | ActiveRecord::Schema.define do 24 | create_table "users", :force => true do |t| 25 | t.string "login" 26 | t.datetime "created_at" 27 | t.datetime "updated_at" 28 | end 29 | 30 | create_table "profiles", :force => true do |t| 31 | t.integer "user_id" 32 | t.string "name" 33 | t.string "homepage" 34 | end 35 | 36 | create_table "emails", :force => true do |t| 37 | t.integer "user_id" 38 | t.string "email" 39 | t.datetime "created_at" 40 | end 41 | 42 | if version[0,3] > '2.2' 43 | create_table "domains", :force => true do |t| 44 | t.string "host" 45 | end 46 | 47 | create_table "web_pages", :force => true do |t| 48 | t.string "url" 49 | t.string "domain_host" 50 | end 51 | end 52 | 53 | create_table "notes", :force => true do |t| 54 | t.integer "notable_id" 55 | t.string "notable_type" 56 | end 57 | 58 | create_table "namespaced", :force => true 59 | end 60 | 61 | # models 62 | class User < ActiveRecord::Base 63 | has_one :profile, :dependent => :destroy 64 | has_many :emails, :dependent => :destroy, :order => 'id' 65 | has_many :notes, :as => :notable 66 | replicate_natural_key :login 67 | end 68 | 69 | class Profile < ActiveRecord::Base 70 | belongs_to :user 71 | replicate_natural_key :user_id 72 | end 73 | 74 | class Email < ActiveRecord::Base 75 | belongs_to :user 76 | replicate_natural_key :user_id, :email 77 | end 78 | 79 | if version[0,3] > '2.2' 80 | class WebPage < ActiveRecord::Base 81 | belongs_to :domain, :foreign_key => 'domain_host', :primary_key => 'host' 82 | end 83 | 84 | class Domain < ActiveRecord::Base 85 | replicate_natural_key :host 86 | end 87 | end 88 | 89 | class Note < ActiveRecord::Base 90 | belongs_to :notable, :polymorphic => true 91 | end 92 | 93 | class User::Namespaced < ActiveRecord::Base 94 | self.table_name = "namespaced" 95 | end 96 | 97 | # The test case loads some fixture data once and uses transaction rollback to 98 | # reset fixture state for each test's setup. 99 | class ActiveRecordTest < Test::Unit::TestCase 100 | def setup 101 | self.class.fixtures 102 | ActiveRecord::Base.connection.increment_open_transactions 103 | ActiveRecord::Base.connection.begin_db_transaction 104 | 105 | @rtomayko = User.find_by_login('rtomayko') 106 | @kneath = User.find_by_login('kneath') 107 | @tmm1 = User.find_by_login('tmm1') 108 | 109 | User.replicate_associations = [] 110 | 111 | @dumper = Replicate::Dumper.new 112 | @loader = Replicate::Loader.new 113 | end 114 | 115 | def teardown 116 | ActiveRecord::Base.connection.rollback_db_transaction 117 | ActiveRecord::Base.connection.decrement_open_transactions 118 | end 119 | 120 | def self.fixtures 121 | return if @fixtures 122 | @fixtures = true 123 | user = User.create! :login => 'rtomayko' 124 | user.create_profile :name => 'Ryan Tomayko', :homepage => 'http://tomayko.com' 125 | user.emails.create! :email => 'ryan@github.com' 126 | user.emails.create! :email => 'rtomayko@gmail.com' 127 | 128 | user = User.create! :login => 'kneath' 129 | user.create_profile :name => 'Kyle Neath', :homepage => 'http://warpspire.com' 130 | user.emails.create! :email => 'kyle@github.com' 131 | 132 | user = User.create! :login => 'tmm1' 133 | user.create_profile :name => 'tmm1', :homepage => 'https://github.com/tmm1' 134 | 135 | if defined?(Domain) 136 | github = Domain.create! :host => 'github.com' 137 | github_about_page = WebPage.create! :url => 'http://github.com/about', :domain => github 138 | end 139 | end 140 | 141 | def test_extension_modules_loaded 142 | assert User.respond_to?(:load_replicant) 143 | assert User.new.respond_to?(:dump_replicant) 144 | end 145 | 146 | def test_auto_dumping_belongs_to_associations 147 | objects = [] 148 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 149 | 150 | rtomayko = User.find_by_login('rtomayko') 151 | @dumper.dump rtomayko.profile 152 | 153 | assert_equal 2, objects.size 154 | 155 | type, id, attrs, obj = objects.shift 156 | assert_equal 'User', type 157 | assert_equal rtomayko.id, id 158 | assert_equal 'rtomayko', attrs['login'] 159 | assert_equal rtomayko.created_at, attrs['created_at'] 160 | assert_equal rtomayko, obj 161 | 162 | type, id, attrs, obj = objects.shift 163 | assert_equal 'Profile', type 164 | assert_equal rtomayko.profile.id, id 165 | assert_equal 'Ryan Tomayko', attrs['name'] 166 | assert_equal rtomayko.profile, obj 167 | end 168 | 169 | def test_omit_dumping_of_attribute 170 | objects = [] 171 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 172 | 173 | User.replicate_omit_attributes :created_at 174 | rtomayko = User.find_by_login('rtomayko') 175 | @dumper.dump rtomayko 176 | 177 | assert_equal 2, objects.size 178 | 179 | type, id, attrs, obj = objects.shift 180 | assert_equal nil, attrs['created_at'] 181 | end 182 | 183 | def test_omit_dumping_of_association 184 | objects = [] 185 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 186 | 187 | User.replicate_omit_attributes :profile 188 | rtomayko = User.find_by_login('rtomayko') 189 | @dumper.dump rtomayko 190 | 191 | assert_equal 1, objects.size 192 | 193 | type, id, attrs, obj = objects.shift 194 | assert_equal 'User', type 195 | end 196 | 197 | if ActiveRecord::VERSION::STRING[0, 3] > '2.2' 198 | def test_dump_and_load_non_standard_foreign_key_association 199 | objects = [] 200 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 201 | 202 | github_about_page = WebPage.find_by_url('http://github.com/about') 203 | assert_equal "github.com", github_about_page.domain.host 204 | @dumper.dump github_about_page 205 | 206 | WebPage.delete_all 207 | Domain.delete_all 208 | 209 | # load everything back up 210 | objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs } 211 | 212 | github_about_page = WebPage.find_by_url('http://github.com/about') 213 | assert_equal "github.com", github_about_page.domain_host 214 | assert_equal "github.com", github_about_page.domain.host 215 | end 216 | end 217 | 218 | def test_auto_dumping_has_one_associations 219 | objects = [] 220 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 221 | 222 | rtomayko = User.find_by_login('rtomayko') 223 | @dumper.dump rtomayko 224 | 225 | assert_equal 2, objects.size 226 | 227 | type, id, attrs, obj = objects.shift 228 | assert_equal 'User', type 229 | assert_equal rtomayko.id, id 230 | assert_equal 'rtomayko', attrs['login'] 231 | assert_equal rtomayko.created_at, attrs['created_at'] 232 | assert_equal rtomayko, obj 233 | 234 | type, id, attrs, obj = objects.shift 235 | assert_equal 'Profile', type 236 | assert_equal rtomayko.profile.id, id 237 | assert_equal 'Ryan Tomayko', attrs['name'] 238 | assert_equal [:id, 'User', rtomayko.id], attrs['user_id'] 239 | assert_equal rtomayko.profile, obj 240 | end 241 | 242 | def test_auto_dumping_does_not_fail_on_polymorphic_associations 243 | objects = [] 244 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 245 | 246 | rtomayko = User.find_by_login('rtomayko') 247 | note = Note.create!(:notable => rtomayko) 248 | @dumper.dump note 249 | 250 | assert_equal 3, objects.size 251 | 252 | type, id, attrs, obj = objects.shift 253 | assert_equal 'User', type 254 | assert_equal rtomayko.id, id 255 | 256 | type, id, attrs, obj = objects.shift 257 | assert_equal 'Profile', type 258 | 259 | type, id, attrs, obj = objects.shift 260 | assert_equal 'Note', type 261 | assert_equal note.id, id 262 | assert_equal note.notable_type, attrs['notable_type'] 263 | assert_equal attrs["notable_id"], [:id, 'User', rtomayko.id] 264 | assert_equal note, obj 265 | end 266 | 267 | def test_dumping_has_many_associations 268 | objects = [] 269 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 270 | 271 | User.replicate_associations :emails 272 | rtomayko = User.find_by_login('rtomayko') 273 | @dumper.dump rtomayko 274 | 275 | assert_equal 4, objects.size 276 | 277 | type, id, attrs, obj = objects.shift 278 | assert_equal 'User', type 279 | assert_equal rtomayko.id, id 280 | assert_equal 'rtomayko', attrs['login'] 281 | assert_equal rtomayko.created_at, attrs['created_at'] 282 | assert_equal rtomayko, obj 283 | 284 | type, id, attrs, obj = objects.shift 285 | assert_equal 'Profile', type 286 | assert_equal rtomayko.profile.id, id 287 | assert_equal 'Ryan Tomayko', attrs['name'] 288 | assert_equal rtomayko.profile, obj 289 | 290 | type, id, attrs, obj = objects.shift 291 | assert_equal 'Email', type 292 | assert_equal 'ryan@github.com', attrs['email'] 293 | assert_equal [:id, 'User', rtomayko.id], attrs['user_id'] 294 | assert_equal rtomayko.emails.first, obj 295 | 296 | type, id, attrs, obj = objects.shift 297 | assert_equal 'Email', type 298 | assert_equal 'rtomayko@gmail.com', attrs['email'] 299 | assert_equal [:id, 'User', rtomayko.id], attrs['user_id'] 300 | assert_equal rtomayko.emails.last, obj 301 | end 302 | 303 | def test_dumping_associations_at_dump_time 304 | objects = [] 305 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 306 | 307 | rtomayko = User.find_by_login('rtomayko') 308 | @dumper.dump rtomayko, :associations => [:emails], :omit => [:profile] 309 | 310 | assert_equal 3, objects.size 311 | 312 | type, id, attrs, obj = objects.shift 313 | assert_equal 'User', type 314 | assert_equal rtomayko.id, id 315 | assert_equal 'rtomayko', attrs['login'] 316 | assert_equal rtomayko.created_at, attrs['created_at'] 317 | assert_equal rtomayko, obj 318 | 319 | type, id, attrs, obj = objects.shift 320 | assert_equal 'Email', type 321 | assert_equal 'ryan@github.com', attrs['email'] 322 | assert_equal [:id, 'User', rtomayko.id], attrs['user_id'] 323 | assert_equal rtomayko.emails.first, obj 324 | 325 | type, id, attrs, obj = objects.shift 326 | assert_equal 'Email', type 327 | assert_equal 'rtomayko@gmail.com', attrs['email'] 328 | assert_equal [:id, 'User', rtomayko.id], attrs['user_id'] 329 | assert_equal rtomayko.emails.last, obj 330 | end 331 | 332 | def test_dumping_many_associations_at_dump_time 333 | objects = [] 334 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 335 | 336 | users = User.all(:conditions => {:login => %w[rtomayko kneath]}) 337 | @dumper.dump users, :associations => [:emails], :omit => [:profile] 338 | 339 | assert_equal 5, objects.size 340 | assert_equal ['Email', 'Email', 'Email', 'User', 'User'], objects.map { |type,_,_| type }.sort 341 | end 342 | 343 | def test_omit_attributes_at_dump_time 344 | objects = [] 345 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 346 | 347 | rtomayko = User.find_by_login('rtomayko') 348 | @dumper.dump rtomayko, :omit => [:created_at] 349 | 350 | type, id, attrs, obj = objects.shift 351 | assert_equal 'User', type 352 | assert attrs['updated_at'] 353 | assert_nil attrs['created_at'] 354 | end 355 | 356 | def test_dumping_polymorphic_associations 357 | objects = [] 358 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 359 | 360 | User.replicate_associations :notes 361 | rtomayko = User.find_by_login('rtomayko') 362 | note = Note.create!(:notable => rtomayko) 363 | @dumper.dump rtomayko 364 | 365 | assert_equal 3, objects.size 366 | 367 | type, id, attrs, obj = objects.shift 368 | assert_equal 'User', type 369 | assert_equal rtomayko.id, id 370 | assert_equal 'rtomayko', attrs['login'] 371 | assert_equal rtomayko.created_at, attrs['created_at'] 372 | assert_equal rtomayko, obj 373 | 374 | type, id, attrs, obj = objects.shift 375 | assert_equal 'Profile', type 376 | assert_equal rtomayko.profile.id, id 377 | assert_equal 'Ryan Tomayko', attrs['name'] 378 | assert_equal rtomayko.profile, obj 379 | 380 | type, id, attrs, obj = objects.shift 381 | assert_equal 'Note', type 382 | assert_equal note.notable_type, attrs['notable_type'] 383 | assert_equal [:id, 'User', rtomayko.id], attrs['notable_id'] 384 | assert_equal rtomayko.notes.first, obj 385 | 386 | end 387 | 388 | def test_dumping_empty_polymorphic_associations 389 | objects = [] 390 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 391 | 392 | note = Note.create!() 393 | @dumper.dump note 394 | 395 | assert_equal 1, objects.size 396 | 397 | type, id, attrs, obj = objects.shift 398 | assert_equal 'Note', type 399 | assert_equal nil, attrs['notable_type'] 400 | assert_equal nil, attrs['notable_id'] 401 | end 402 | 403 | def test_dumps_polymorphic_namespaced_associations 404 | objects = [] 405 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 406 | 407 | note = Note.create! :notable => User::Namespaced.create! 408 | @dumper.dump note 409 | 410 | assert_equal 2, objects.size 411 | 412 | type, id, attrs, obj = objects.shift 413 | assert_equal 'User::Namespaced', type 414 | 415 | type, id, attrs, obj = objects.shift 416 | assert_equal 'Note', type 417 | end 418 | 419 | def test_skips_belongs_to_information_if_omitted 420 | objects = [] 421 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 422 | 423 | Profile.replicate_omit_attributes :user 424 | @dumper.dump @rtomayko.profile 425 | 426 | assert_equal 1, objects.size 427 | type, id, attrs, obj = objects.shift 428 | assert_equal @rtomayko.profile.user_id, attrs["user_id"] 429 | end 430 | 431 | def test_loading_everything 432 | objects = [] 433 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 434 | 435 | # dump all users and associated objects and destroy 436 | User.replicate_associations :emails 437 | dumped_users = {} 438 | %w[rtomayko kneath tmm1].each do |login| 439 | user = User.find_by_login(login) 440 | @dumper.dump user 441 | user.destroy 442 | dumped_users[login] = user 443 | end 444 | assert_equal 9, objects.size 445 | 446 | # insert another record to ensure id changes for loaded records 447 | sr = User.create!(:login => 'sr') 448 | sr.create_profile :name => 'Simon Rozet' 449 | sr.emails.create :email => 'sr@github.com' 450 | 451 | # load everything back up 452 | objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs } 453 | 454 | # verify attributes are set perfectly again 455 | user = User.find_by_login('rtomayko') 456 | assert_equal 'rtomayko', user.login 457 | assert_equal dumped_users['rtomayko'].created_at, user.created_at 458 | assert_equal dumped_users['rtomayko'].updated_at, user.updated_at 459 | assert_equal 'Ryan Tomayko', user.profile.name 460 | assert_equal 2, user.emails.size 461 | 462 | # make sure everything was recreated 463 | %w[rtomayko kneath tmm1].each do |login| 464 | user = User.find_by_login(login) 465 | assert_not_nil user 466 | assert_not_nil user.profile 467 | assert !user.emails.empty?, "#{login} has no emails" if login != 'tmm1' 468 | end 469 | end 470 | 471 | def test_loading_with_existing_records 472 | objects = [] 473 | @dumper.listen { |type, id, attrs, obj| objects << [type, id, attrs, obj] } 474 | 475 | # dump all users and associated objects and destroy 476 | User.replicate_associations :emails 477 | dumped_users = {} 478 | %w[rtomayko kneath tmm1].each do |login| 479 | user = User.find_by_login(login) 480 | user.profile.update_attribute :name, 'CHANGED' 481 | @dumper.dump user 482 | dumped_users[login] = user 483 | end 484 | assert_equal 9, objects.size 485 | 486 | # load everything back up 487 | objects.each { |type, id, attrs, obj| @loader.feed type, id, attrs } 488 | 489 | # ensure additional objects were not created 490 | assert_equal 3, User.count 491 | 492 | # verify attributes are set perfectly again 493 | user = User.find_by_login('rtomayko') 494 | assert_equal 'rtomayko', user.login 495 | assert_equal dumped_users['rtomayko'].created_at, user.created_at 496 | assert_equal dumped_users['rtomayko'].updated_at, user.updated_at 497 | assert_equal 'CHANGED', user.profile.name 498 | assert_equal 2, user.emails.size 499 | 500 | # make sure everything was recreated 501 | %w[rtomayko kneath tmm1].each do |login| 502 | user = User.find_by_login(login) 503 | assert_not_nil user 504 | assert_not_nil user.profile 505 | assert_equal 'CHANGED', user.profile.name 506 | assert !user.emails.empty?, "#{login} has no emails" if login != 'tmm1' 507 | end 508 | end 509 | 510 | def test_loading_with_replicating_id 511 | objects = [] 512 | @dumper.listen do |type, id, attrs, obj| 513 | objects << [type, id, attrs, obj] if type == 'User' 514 | end 515 | 516 | dumped_users = {} 517 | %w[rtomayko kneath tmm1].each do |login| 518 | user = User.find_by_login(login) 519 | @dumper.dump user 520 | dumped_users[login] = user 521 | end 522 | assert_equal 3, objects.size 523 | 524 | User.destroy_all 525 | User.replicate_id = false 526 | 527 | # load everything back up 528 | objects.each { |type, id, attrs, obj| User.load_replicant type, id, attrs } 529 | 530 | user = User.find_by_login('rtomayko') 531 | assert_not_equal dumped_users['rtomayko'].id, user.id 532 | 533 | User.destroy_all 534 | User.replicate_id = true 535 | 536 | # load everything back up 537 | objects.each { |type, id, attrs, obj| User.load_replicant type, id, attrs } 538 | 539 | user = User.find_by_login('rtomayko') 540 | assert_equal dumped_users['rtomayko'].id, user.id 541 | end 542 | 543 | def test_loader_saves_without_validations 544 | # note when a record is saved with validations 545 | ran_validations = false 546 | User.class_eval { validate { ran_validations = true } } 547 | 548 | # check our assumptions 549 | user = User.create(:login => 'defunkt') 550 | assert ran_validations, "should run validations here" 551 | ran_validations = false 552 | 553 | # load one and verify validations are not run 554 | user = nil 555 | @loader.listen { |type, id, attrs, obj| user = obj } 556 | @loader.feed 'User', 1, 'login' => 'rtomayko' 557 | assert_not_nil user 558 | assert !ran_validations, 'validations should not run on save' 559 | end 560 | 561 | def test_loader_saves_without_callbacks 562 | # note when a record is saved with callbacks 563 | callbacks = false 564 | User.class_eval { after_save { callbacks = true } } 565 | User.class_eval { after_create { callbacks = true } } 566 | User.class_eval { after_update { callbacks = true } } 567 | User.class_eval { after_commit { callbacks = true } } 568 | 569 | # check our assumptions 570 | user = User.create(:login => 'defunkt') 571 | assert callbacks, "should run callbacks here" 572 | callbacks = false 573 | 574 | # load one and verify validations are not run 575 | user = nil 576 | @loader.listen { |type, id, attrs, obj| user = obj } 577 | @loader.feed 'User', 1, 'login' => 'rtomayko' 578 | assert_not_nil user 579 | assert !callbacks, 'callbacks should not run on save' 580 | end 581 | 582 | def test_loader_saves_without_updating_created_at_timestamp 583 | timestamp = Time.at((Time.now - (24 * 60 * 60)).to_i) 584 | user = nil 585 | @loader.listen { |type, id, attrs, obj| user = obj } 586 | @loader.feed 'User', 23, 'login' => 'brianmario', 'created_at' => timestamp 587 | assert_equal timestamp, user.created_at 588 | user = User.find(user.id) 589 | assert_equal timestamp, user.created_at 590 | end 591 | 592 | def test_loader_saves_without_updating_updated_at_timestamp 593 | timestamp = Time.at((Time.now - (24 * 60 * 60)).to_i) 594 | user = nil 595 | @loader.listen { |type, id, attrs, obj| user = obj } 596 | @loader.feed 'User', 29, 'login' => 'rtomayko', 'updated_at' => timestamp 597 | assert_equal timestamp, user.updated_at 598 | user = User.find(user.id) 599 | assert_equal timestamp, user.updated_at 600 | end 601 | 602 | def test_enabling_active_record_query_cache 603 | ActiveRecord::Base.connection.enable_query_cache! 604 | ActiveRecord::Base.connection.disable_query_cache! 605 | end 606 | end 607 | -------------------------------------------------------------------------------- /test/custom_objects_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'stringio' 3 | require 'replicate' 4 | 5 | class MyCustomObject 6 | attr_accessor :custom_val 7 | 8 | def dump_replicant(dumper) 9 | attributes = { 'test' => 'value' } 10 | dumper.write self.class, 3, attributes, self 11 | end 12 | 13 | def self.load_replicant(type, id, attributes) 14 | @test = attributes['context'] 15 | @test.assert_equal 5, id 16 | @test.assert_equal 'value', attributes['test'] 17 | @test.assert_equal 'MyCustomObject', type 18 | obj = MyCustomObject.new 19 | obj.custom_val = 'custom' 20 | [id, obj] 21 | end 22 | end 23 | 24 | class CustomObjectsTest < Test::Unit::TestCase 25 | def test_custom_dump 26 | @dumper = Replicate::Dumper.new 27 | called = false 28 | object = MyCustomObject.new 29 | @dumper.listen do |type, id, attrs, obj| 30 | assert !called 31 | assert_equal 'MyCustomObject', type 32 | assert_equal 3, id 33 | assert_equal({ 'test' => 'value' }, attrs) 34 | called = true 35 | end 36 | @dumper.dump object 37 | assert called 38 | end 39 | 40 | def test_custom_load 41 | @loader = Replicate::Loader.new 42 | called = false 43 | object = MyCustomObject.new 44 | @loader.listen do |type, id, attrs, obj| 45 | assert !called 46 | assert_equal 'MyCustomObject', type 47 | assert_equal 'custom', obj.custom_val 48 | called = true 49 | end 50 | @loader.feed object.class, 5, { 'test' => 'value', 'context' => self } 51 | assert called 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/dumper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'stringio' 3 | require 'replicate' 4 | 5 | class DumperTest < Test::Unit::TestCase 6 | def setup 7 | @dumper = Replicate::Dumper.new 8 | end 9 | 10 | def thing(attrs={}) 11 | attrs = {'number' => 123, 'string' => 'hello', 'time' => Time.new}.merge(attrs) 12 | Replicate::Object.new attrs 13 | end 14 | 15 | def test_basic_filter 16 | called = false 17 | object = thing('test' => 'value') 18 | @dumper.listen do |type, id, attrs, obj| 19 | assert !called 20 | assert_equal 'Replicate::Object', type 21 | assert_equal object.id, id 22 | assert_equal 'value', attrs['test'] 23 | assert_equal object.attributes, attrs 24 | called = true 25 | end 26 | @dumper.dump object 27 | assert called 28 | end 29 | 30 | def test_failure_when_object_not_respond_to_dump_replicant 31 | assert_raise(NoMethodError) { @dumper.dump Object.new } 32 | end 33 | 34 | def test_never_dumps_objects_more_than_once 35 | called = false 36 | object = thing('test' => 'value') 37 | @dumper.listen do |type, id, attrs, obj| 38 | assert !called 39 | called = true 40 | end 41 | @dumper.dump object 42 | @dumper.dump object 43 | @dumper.dump object 44 | assert called 45 | end 46 | 47 | def test_writing_to_io 48 | io = StringIO.new 49 | io.set_encoding 'BINARY' if io.respond_to?(:set_encoding) 50 | @dumper.marshal_to io 51 | @dumper.dump object = thing 52 | data = Marshal.dump(['Replicate::Object', object.id, object.attributes]) 53 | assert_equal data, io.string 54 | end 55 | 56 | def test_stats 57 | 10.times { @dumper.dump thing } 58 | assert_equal({'Replicate::Object' => 10}, @dumper.stats) 59 | end 60 | 61 | def test_block_form_runs_complete 62 | called = false 63 | Replicate::Dumper.new do |dumper| 64 | filter = lambda { |*args| } 65 | (class < 'hello') 2 | -------------------------------------------------------------------------------- /test/linked_dumpscript.rb: -------------------------------------------------------------------------------- 1 | load_script File.expand_path('../dumpscript.rb', __FILE__) 2 | -------------------------------------------------------------------------------- /test/loader_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'stringio' 3 | require 'replicate' 4 | 5 | class LoaderTest < Test::Unit::TestCase 6 | def setup 7 | @loader = Replicate::Loader.new 8 | end 9 | 10 | def thing(attrs={}) 11 | attrs = {'number' => 123, 'string' => 'hello', 'time' => Time.new}.merge(attrs) 12 | Replicate::Object.new attrs 13 | end 14 | 15 | def test_basic_filter 16 | called = false 17 | object = thing('test' => 'value') 18 | @loader.listen do |type, id, attrs, obj| 19 | assert !called 20 | assert_equal 'Replicate::Object', type 21 | assert_equal object.id, id 22 | assert_equal 'value', attrs['test'] 23 | assert_equal object.attributes, attrs 24 | called = true 25 | end 26 | @loader.feed object.class, object.id, object.attributes 27 | assert called 28 | end 29 | 30 | def test_reading_from_io 31 | called = false 32 | data = Marshal.dump(['Replicate::Object', 10, {'test' => 'value'}]) 33 | @loader.listen do |type, id, attrs, obj| 34 | assert !called 35 | assert_equal 'Replicate::Object', type 36 | assert_equal 'value', attrs['test'] 37 | called = true 38 | end 39 | @loader.read(StringIO.new(data)) 40 | assert called 41 | end 42 | 43 | def test_stats 44 | 10.times do 45 | obj = thing 46 | @loader.feed obj.class, obj.id, obj.attributes 47 | end 48 | assert_equal({'Replicate::Object' => 10}, @loader.stats) 49 | end 50 | 51 | def test_block_form_runs_complete 52 | called = false 53 | Replicate::Loader.new do |loader| 54 | filter = lambda { |*args| } 55 | (class < [:id, 'Replicate::Object', object1.id]) 71 | @loader.feed object2.class, object2.id, object2.attributes 72 | 73 | assert_equal 2, objects.size 74 | assert_equal objects[0].id, objects[1].related 75 | end 76 | 77 | def test_translating_multiple_id_attributes 78 | objects = [] 79 | @loader.listen { |type, id, attrs, object| objects << object } 80 | 81 | members = (0..9).map { |i| thing('number' => i) } 82 | members.each do |member| 83 | @loader.feed member.class, member.id, member.attributes 84 | end 85 | 86 | ids = members.map { |m| m.id } 87 | referencer = thing('related' => [:id, 'Replicate::Object', ids]) 88 | @loader.feed referencer.class, referencer.id, referencer.attributes 89 | 90 | assert_equal 11, objects.size 91 | assert_equal 10, objects.last.related.size 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/replicate_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class ReplicateTest < Test::Unit::TestCase 4 | def test_auto_loading 5 | require 'replicate' 6 | Replicate::Dumper 7 | Replicate::Loader 8 | Replicate::Status 9 | end 10 | end 11 | --------------------------------------------------------------------------------