├── .gitignore ├── .travis.yml ├── CHANGES ├── LICENSE ├── Makefile ├── README.rdoc ├── Rakefile ├── ci_build ├── db-charmer.gemspec ├── init.rb ├── issues └── issues-as-of-2014-11-14.json ├── lib ├── db_charmer.rb └── db_charmer │ ├── action_controller │ └── force_slave_reads.rb │ ├── active_record │ ├── association_preload.rb │ ├── class_attributes.rb │ ├── connection_switching.rb │ ├── db_magic.rb │ ├── migration │ │ └── multi_db_migrations.rb │ ├── multi_db_proxy.rb │ └── sharding.rb │ ├── connection_factory.rb │ ├── connection_proxy.rb │ ├── core_extensions.rb │ ├── force_slave_reads.rb │ ├── rails2 │ ├── abstract_adapter │ │ └── log_formatting.rb │ └── active_record │ │ ├── master_slave_routing.rb │ │ └── named_scope │ │ └── scope_proxy.rb │ ├── rails3 │ ├── abstract_adapter │ │ └── connection_name.rb │ └── active_record │ │ ├── log_subscriber.rb │ │ ├── master_slave_routing.rb │ │ ├── relation │ │ └── connection_routing.rb │ │ └── relation_method.rb │ ├── rails31 │ └── active_record │ │ ├── migration │ │ └── command_recorder.rb │ │ └── preloader │ │ ├── association.rb │ │ └── has_and_belongs_to_many.rb │ ├── railtie.rb │ ├── sharding.rb │ ├── sharding │ ├── connection.rb │ ├── method.rb │ ├── method │ │ ├── db_block_group_map.rb │ │ ├── db_block_map.rb │ │ ├── hash_map.rb │ │ └── range.rb │ └── stub_connection.rb │ ├── tasks │ └── databases.rake │ ├── version.rb │ └── with_remapped_databases.rb ├── test-project-2.x ├── .gitignore ├── .rspec ├── Gemfile ├── Rakefile ├── app ├── config │ ├── boot.rb │ ├── database.yml.example │ ├── environment.rb │ ├── environments │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── db_charmer.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── new_rails_defaults.rb │ │ ├── session_store.rb │ │ └── sharding.rb │ ├── locales │ │ └── en.yml │ ├── preinitializer.rb │ └── routes.rb ├── db ├── script │ └── console └── spec │ ├── controllers │ ├── fixtures │ ├── models │ ├── sharding │ ├── spec.opts │ ├── spec_helper.rb │ ├── support │ └── unit └── test-project ├── .gitignore ├── .rspec ├── Gemfile ├── Rakefile ├── TODO ├── app ├── controllers │ ├── application_controller.rb │ └── posts_controller.rb ├── helpers │ └── application_helper.rb ├── models │ ├── avatar.rb │ ├── car.rb │ ├── categories_posts.rb │ ├── category.rb │ ├── comment.rb │ ├── event.rb │ ├── ford.rb │ ├── house.rb │ ├── log_record.rb │ ├── post.rb │ ├── range_sharded_model.rb │ ├── toyota.rb │ └── user.rb └── views │ ├── layouts │ └── application.html.erb │ └── posts │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb ├── config ├── application.rb ├── boot.rb ├── database.yml.example ├── environment.rb ├── environments │ └── test.rb ├── initializers │ ├── backtrace_silencers.rb │ ├── db_charmer.rb │ ├── secret_token.rb │ ├── session_store.rb │ └── sharding.rb ├── locales │ └── en.yml └── routes.rb ├── db ├── create_databases.sql ├── migrate │ ├── 20090810013829_create_log_records.rb │ ├── 20090810013922_create_posts.rb │ ├── 20090810221944_create_users.rb │ ├── 20100305234245_create_categories.rb │ ├── 20100305234340_create_categories_posts.rb │ ├── 20100305235831_create_avatars.rb │ ├── 20100328201317_create_sharding_map_tables.rb │ ├── 20100330180517_create_event_tables.rb │ ├── 20100817191548_create_cars.rb │ └── 20111005193941_create_comments.rb ├── seeds.rb └── sharding.sql └── spec ├── controllers └── posts_controller_spec.rb ├── fixtures ├── avatars.yml ├── categories.yml ├── categories_posts.yml ├── comments.yml ├── event_shards_info.yml ├── event_shards_map.yml ├── log_records.yml ├── posts.yml └── users.yml ├── integration └── multi_threading_spec.rb ├── models ├── avatar_spec.rb ├── cars_spec.rb ├── categories_posts_spec.rb ├── category_spec.rb ├── comment_spec.rb ├── event_spec.rb ├── log_record_spec.rb ├── post_spec.rb ├── range_sharded_model_spec.rb └── user_spec.rb ├── sharding ├── connection_spec.rb ├── method │ ├── db_block_map_spec.rb │ ├── hash_map_spec.rb │ └── range_spec.rb └── sharding_spec.rb ├── spec_helper.rb ├── support └── rails31_stub_connection.rb └── unit ├── abstract_adapter └── log_formatting_spec.rb ├── action_controller └── force_slave_reads_spec.rb ├── active_record ├── association_preload_spec.rb ├── association_proxy_spec.rb ├── class_attributes_spec.rb ├── connection_switching_spec.rb ├── db_magic_spec.rb ├── master_slave_routing_spec.rb ├── migration │ └── multi_db_migrations_spec.rb ├── named_scope │ └── named_scope_spec.rb └── relation_spec.rb ├── connection_factory_spec.rb ├── connection_proxy_spec.rb ├── db_charmer_spec.rb ├── multi_db_proxy_spec.rb └── with_remapped_databases_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | pkg 3 | .DS_Store 4 | _site 5 | .idea 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - 2.0.0 6 | 7 | env: 8 | - RAILS_VERSION=2.x 9 | - RAILS_VERSION=3.0.20 10 | - RAILS_VERSION=3.1.12 11 | - RAILS_VERSION=3.2.3 12 | - RAILS_VERSION=3.2.15 13 | - RAILS_VERSION=3.2.15 DB_CHARMER_GEM=1.9.0 14 | 15 | notifications: 16 | recipients: 17 | - alexey@kovyrin.net 18 | 19 | script: ./ci_build 20 | 21 | # Whitelist branches to test 22 | branches: 23 | only: 24 | - master 25 | - rails4 26 | 27 | # Build matrix configuration 28 | matrix: 29 | exclude: 30 | # Do not run Rails 2.x tests on ruby 1.9 31 | - rvm: 1.9.3 32 | env: RAILS_VERSION=2.x 33 | 34 | # Do not run Rails 2.x tests on ruby 2.0 35 | - rvm: 2.0.0 36 | env: RAILS_VERSION=2.x 37 | 38 | # Do not run Rails 3.0 tests on ruby 2.0 39 | - rvm: 2.0.0 40 | env: RAILS_VERSION=3.0.20 41 | 42 | # Do not run Rails 3.1 tests on ruby 2.0 43 | - rvm: 2.0.0 44 | env: RAILS_VERSION=3.1.12 45 | 46 | # Do not run early Rails 3.2 tests on ruby 2.0 47 | - rvm: 2.0.0 48 | env: RAILS_VERSION=3.2.3 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011, Oleksiy Kovyrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | doc/files/README_rdoc.html: README.rdoc 2 | rdoc README.rdoc -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = WARNING: The Project Has Been Suspended 2 | 3 | Please note, that this project has been suspended. No updates will be provided and no Rails versions 4 | beyond 3.2.x will be supported. For more information please check out this blog post: http://kovyrin.net/2014/11/14/dbcharmer-suspended/ 5 | 6 | = DB Charmer - ActiveRecord Connection Magic Plugin 7 | 8 | +DbCharmer+ is a simple yet powerful plugin for ActiveRecord that significantly extends its ability to work with 9 | multiple databases and/or database servers. The major features we add to ActiveRecord are: 10 | 11 | 1. Simple management for AR model connections (+switch_connection_to+ method) 12 | 2. Switching of default AR model connections to separate servers/databases 13 | 3. Ability to easily choose where your query should go (Model.on_* methods family) 14 | 4. Automated master/slave queries routing (selects go to a slave, updates handled by the master). 15 | 5. Multiple database migrations with very flexible query routing controls. 16 | 6. Simple database sharding functionality with multiple sharding methods (value, range, mapping table). 17 | 18 | For more information on the project, you can check out our web site at http://kovyrin.github.io/db-charmer/. 19 | 20 | == Installation 21 | 22 | There are two options when approaching +DbCharmer+ installation: 23 | * using the gem (recommended and the only way of using it with Rails 3.2+) 24 | * install as a Rails plugin (works in Rails 2.x only) 25 | 26 | To install as a gem, add this to your Gemfile: 27 | 28 | gem 'db-charmer', :require => 'db_charmer' 29 | 30 | To install +DbCharmer+ as a Rails plugin use the following command: 31 | 32 | ./script/plugin install git://github.com/kovyrin/db-charmer.git 33 | 34 | _Notice_: If you use +DbCharmer+ in a non-rails project, you may need to set DbCharmer.env to a correct value 35 | before using any of its connection management methods. Correct value here is a valid database.yml 36 | first-level section name. 37 | 38 | 39 | == Documentation/Questions 40 | 41 | For more information about the library, please visit our site at http://dbcharmer.net. 42 | If you need more defails on DbCharmer internals, please check out the source code. All the plugin's 43 | code is ~100% covered with tests. The project located in test-project directory has unit 44 | tests for all or, at least, the most actively used code paths. 45 | 46 | If you have any questions regarding this project, you could contact the author using 47 | the DbCharmer Users Group mailing list: 48 | 49 | - Group Info: http://groups.google.com/group/db-charmer 50 | - Subscribe using the info page or by sending an email to mailto:db-charmer-subscribe@googlegroups.com 51 | 52 | 53 | == What Ruby and Rails implementations does it work for? 54 | 55 | We have a continuous integration setup for this gem on with Rails 2.3, 3.0, 3.1 and 3.2 using a few 56 | different versions of Ruby. 57 | 58 | CI is running on TravisCI.org: https://travis-ci.org/kovyrin/db-charmer 59 | Build status is: {Build Status: Rails 3.x}[https://travis-ci.org/kovyrin/db-charmer] 60 | 61 | At the moment we have the following build matrix: 62 | * Rails versions: 63 | - 2.3 64 | - 3.0 65 | - 3.1 66 | - 3.2 67 | * Ruby versions: 68 | - 1.8.7 69 | - 1.9.3 (Rails 3.0+ only) 70 | - 2.0.0 (Rails 3.2+ only) 71 | * Databases: 72 | - MySQL 73 | 74 | In addition to CI testing, this gem is used in production on Scribd.com (one of the largest RoR 75 | sites in the world) with Ruby Enterprise Edition and Rails 2.2, Rails 2.3, Sinatra and plain 76 | Rack applications. 77 | 78 | Starting with version 1.8.0 we support Rails versions 3.2.8 and higher. Please note, that Rails 3.2.4 79 | is not officially supported. Your code may work on that version, but no bug reports will be 80 | accepted about this version. 81 | 82 | 83 | == Is it Thread-Safe? 84 | 85 | Starting with version 1.9.0 we have started working on making the code thread-safe and making sure 86 | DbCharmer works correctly in multi-threaded environments. At this moment we consider multi-threaded 87 | mode experimental. If you use it and it works for you - please let us know, if it does not - please 88 | make sure to file a ticket so that we could improve the code and make it work in your situation. 89 | 90 | 91 | == Who are the authors? 92 | 93 | This plugin has been created in Scribd.com for our internal use and then the sources were opened for 94 | other people to use. Most of the code in this package has been developed by Oleksiy Kovyrin for 95 | Scribd.com and is released under the MIT license. For more details, see the LICENSE file. 96 | 97 | Other contributors who have helped with the development of this library are (alphabetically ordered): 98 | * Allen Madsen 99 | * Andrew Geweke 100 | * Ashley Martens 101 | * Cauê Guerra 102 | * David Dai 103 | * Dmytro Shteflyuk 104 | * Eric Lindvall 105 | * Eugene Pimenov 106 | * Jonathan Viney 107 | * Gregory Man 108 | * Michael Birk 109 | * Tyler McMullen 110 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'bundler' 3 | 4 | Bundler::GemHelper.install_tasks 5 | -------------------------------------------------------------------------------- /ci_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Making the script more robust 4 | set -e # Exit on errors 5 | set -u # Exit on uninitialized variables 6 | 7 | RAILS_VERSION=${RAILS_VERSION:-} 8 | if [ "$RAILS_VERSION" == "" ]; then 9 | echo "Please specify rails version using RAILS_VERSION environment variable!" 10 | exit 1 11 | fi 12 | 13 | # Change directory according to the rails version 14 | if [ "$RAILS_VERSION" == "2.x" ]; then 15 | # Downgrade rubygems because rails 2.3 does not work on 2.0+ 16 | gem update --system 1.8.25 17 | cd test-project-2.x 18 | else 19 | cd test-project 20 | fi 21 | 22 | # Print version info 23 | echo "-----------------------------------------------------------------------------------------------------------------" 24 | echo " * Running specs for Rails version $RAILS_VERSION..." 25 | echo " * Ruby version: `ruby --version`" 26 | echo " * Rubygems version: `gem --version`" 27 | echo " * DbCharmer gem version: '${DB_CHARMER_GEM:-trunk}'" 28 | echo "-----------------------------------------------------------------------------------------------------------------" 29 | 30 | # Test environment 31 | export RAILS_ENV=test 32 | 33 | # Configure database access 34 | cp -f config/database.yml.example config/database.yml 35 | 36 | # Create databases and sharding tables 37 | mysql -u root < db/create_databases.sql 38 | mysql -u root db_charmer_sandbox_test < db/sharding.sql 39 | 40 | # Install gems 41 | rm -f Gemfile.lock 42 | bundle install 43 | 44 | # Run migrations 45 | bundle exec rake --trace db:migrate 46 | 47 | # Run the build and return its exit code 48 | if [ "$RAILS_VERSION" == "2.x" ]; then 49 | exec bundle exec spec -p '/*/**/*_spec.rb' -cbfs spec 50 | else 51 | exec bundle exec rspec -cbfs spec 52 | fi 53 | -------------------------------------------------------------------------------- /db-charmer.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'db_charmer/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'db-charmer' 7 | s.version = DbCharmer::Version::STRING 8 | s.platform = Gem::Platform::RUBY 9 | 10 | s.authors = [ 'Oleksiy Kovyrin' ] 11 | s.email = 'alexey@kovyrin.net' 12 | s.homepage = 'http://kovyrin.github.io/db-charmer/' 13 | s.summary = 'ActiveRecord Connections Magic (slaves, multiple connections, etc)' 14 | s.description = 'DbCharmer is a Rails plugin (and gem) that could be used to manage AR model connections, implement master/slave query schemes, sharding and other magic features many high-scale applications need.' 15 | s.license = 'MIT' 16 | 17 | s.rdoc_options = [ '--charset=UTF-8' ] 18 | 19 | s.files = Dir['lib/**/*'] + Dir['*.rb'] 20 | s.files += %w[ README.rdoc LICENSE CHANGES ] 21 | 22 | s.require_paths = [ 'lib' ] 23 | s.extra_rdoc_files = [ 'LICENSE', 'README.rdoc' ] 24 | 25 | # Dependencies 26 | s.add_dependency 'activesupport', '< 4.0.0' 27 | s.add_dependency 'activerecord', '< 4.0.0' 28 | 29 | s.add_development_dependency 'rspec' 30 | s.add_development_dependency 'yard' 31 | s.add_development_dependency 'actionpack' 32 | end 33 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'db_charmer' 2 | -------------------------------------------------------------------------------- /lib/db_charmer/action_controller/force_slave_reads.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActionController 3 | module ForceSlaveReads 4 | 5 | module ClassMethods 6 | @@db_charmer_force_slave_reads_actions = {} 7 | def force_slave_reads(params = {}) 8 | @@db_charmer_force_slave_reads_actions[self.name] = { 9 | :except => params[:except] ? [*params[:except]].map(&:to_s) : [], 10 | :only => params[:only] ? [*params[:only]].map(&:to_s) : [] 11 | } 12 | end 13 | 14 | def force_slave_reads_options 15 | @@db_charmer_force_slave_reads_actions[self.name] 16 | end 17 | 18 | def force_slave_reads_action?(name = nil) 19 | name = name.to_s 20 | 21 | options = force_slave_reads_options 22 | # If no options were defined for this controller, all actions are not forced to use slaves 23 | return false unless options 24 | 25 | # Actions where force_slave_reads mode was turned off 26 | return false if options[:except].include?(name) 27 | 28 | # Only for these actions force_slave_reads was turned on 29 | return options[:only].include?(name) if options[:only].any? 30 | 31 | # If :except is not empty, we're done with the checks and rest of the actions are should force slave reads 32 | # Otherwise, all the actions are not in force_slave_reads mode 33 | options[:except].any? 34 | end 35 | end 36 | 37 | module InstanceMethods 38 | DISPATCH_METHOD = (DbCharmer.rails3?) ? :process_action : :perform_action 39 | 40 | def self.included(base) 41 | base.alias_method_chain DISPATCH_METHOD, :forced_slave_reads 42 | end 43 | 44 | def force_slave_reads! 45 | @db_charmer_force_slave_reads = true 46 | end 47 | 48 | def dont_force_slave_reads! 49 | @db_charmer_force_slave_reads = false 50 | end 51 | 52 | def force_slave_reads? 53 | @db_charmer_force_slave_reads || self.class.force_slave_reads_action?(params[:action]) 54 | end 55 | 56 | protected 57 | 58 | class_eval <<-EOF, __FILE__, __LINE__+1 59 | def #{DISPATCH_METHOD}_with_forced_slave_reads(*args, &block) 60 | DbCharmer.with_controller(self) do 61 | #{DISPATCH_METHOD}_without_forced_slave_reads(*args, &block) 62 | end 63 | end 64 | EOF 65 | end 66 | 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/association_preload.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module AssociationPreload 4 | ASSOCIATION_TYPES = [ :has_one, :has_many, :belongs_to, :has_and_belongs_to_many ] 5 | 6 | def self.extended(base) 7 | ASSOCIATION_TYPES.each do |association_type| 8 | base.class_eval <<-EOF, __FILE__, __LINE__ + 1 9 | def self.preload_#{association_type}_association(records, reflection, preload_options = {}) 10 | if self.db_charmer_top_level_connection? || reflection.options[:polymorphic] || 11 | self.db_charmer_default_connection != reflection.klass.db_charmer_default_connection 12 | return super(records, reflection, preload_options) 13 | end 14 | reflection.klass.on_db(self) do 15 | super(records, reflection, preload_options) 16 | end 17 | end 18 | EOF 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/class_attributes.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module ClassAttributes 4 | @@db_charmer_opts = {} 5 | def db_charmer_opts=(opts) 6 | @@db_charmer_opts[self.name] = opts 7 | end 8 | 9 | def db_charmer_opts 10 | @@db_charmer_opts[self.name] || {} 11 | end 12 | 13 | #--------------------------------------------------------------------------------------------- 14 | @@db_charmer_default_connections = {} 15 | def db_charmer_default_connection=(conn) 16 | @@db_charmer_default_connections[self.name] = conn 17 | end 18 | 19 | def db_charmer_default_connection 20 | @@db_charmer_default_connections[self.name] 21 | end 22 | 23 | #--------------------------------------------------------------------------------------------- 24 | @@db_charmer_slaves = {} 25 | def db_charmer_slaves=(slaves) 26 | @@db_charmer_slaves[self.name] = slaves 27 | end 28 | 29 | def db_charmer_slaves 30 | @@db_charmer_slaves[self.name] || [] 31 | end 32 | 33 | # Returns a random connection from the list of slaves configured for this AR class 34 | def db_charmer_random_slave 35 | return nil unless db_charmer_slaves.any? 36 | db_charmer_slaves[rand(db_charmer_slaves.size)] 37 | end 38 | 39 | #--------------------------------------------------------------------------------------------- 40 | def db_charmer_connection_proxies 41 | Thread.current[:db_charmer_connection_proxies] ||= {} 42 | end 43 | 44 | def db_charmer_connection_proxy=(proxy) 45 | db_charmer_connection_proxies[self.name] = proxy 46 | end 47 | 48 | def db_charmer_connection_proxy 49 | db_charmer_connection_proxies[self.name] 50 | end 51 | 52 | #--------------------------------------------------------------------------------------------- 53 | def db_charmer_force_slave_reads_flags 54 | Thread.current[:db_charmer_force_slave_reads] ||= {} 55 | end 56 | 57 | def db_charmer_force_slave_reads=(force) 58 | db_charmer_force_slave_reads_flags[self.name] = force 59 | end 60 | 61 | def db_charmer_force_slave_reads 62 | db_charmer_force_slave_reads_flags[self.name] 63 | end 64 | 65 | # Slave reads are used in two cases: 66 | # - per-model slave reads are enabled (see db_magic method for more details) 67 | # - global slave reads enforcing is enabled (in a controller action) 68 | def db_charmer_force_slave_reads? 69 | db_charmer_force_slave_reads || DbCharmer.force_slave_reads? 70 | end 71 | 72 | #--------------------------------------------------------------------------------------------- 73 | def db_charmer_connection_levels 74 | Thread.current[:db_charmer_connection_levels] ||= Hash.new(0) 75 | end 76 | 77 | def db_charmer_connection_level=(level) 78 | db_charmer_connection_levels[self.name] = level 79 | end 80 | 81 | def db_charmer_connection_level 82 | db_charmer_connection_levels[self.name] || 0 83 | end 84 | 85 | def db_charmer_top_level_connection? 86 | db_charmer_connection_level.zero? 87 | end 88 | 89 | #--------------------------------------------------------------------------------------------- 90 | def db_charmer_remapped_connection 91 | return nil unless db_charmer_top_level_connection? 92 | name = :master 93 | proxy = db_charmer_model_connection_proxy 94 | name = proxy.db_charmer_connection_name.to_sym if proxy 95 | 96 | remapped = db_charmer_database_remappings[name] 97 | remapped ? DbCharmer::ConnectionFactory.connect(remapped, true) : nil 98 | end 99 | 100 | def db_charmer_database_remappings 101 | Thread.current[:db_charmer_database_remappings] ||= Hash.new 102 | end 103 | 104 | def db_charmer_database_remappings=(mappings) 105 | raise "Mappings must be nil or respond to []" if mappings && (! mappings.respond_to?(:[])) 106 | Thread.current[:db_charmer_database_remappings] = mappings || {} 107 | end 108 | 109 | #--------------------------------------------------------------------------------------------- 110 | # Returns model-specific connection proxy, ignoring any global connection remappings 111 | def db_charmer_model_connection_proxy 112 | db_charmer_connection_proxy || db_charmer_default_connection 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/connection_switching.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module ConnectionSwitching 4 | def establish_real_connection_if_exists(name, should_exist = false) 5 | name = name.to_s 6 | 7 | # Check environment name 8 | config = configurations[DbCharmer.env] 9 | unless config 10 | error = "Invalid environment name (does not exist in database.yml): #{DbCharmer.env}. Please set correct Rails.env or DbCharmer.env." 11 | raise ArgumentError, error 12 | end 13 | 14 | # Check connection name 15 | config = config[name] 16 | unless config 17 | if should_exist 18 | raise ArgumentError, "Invalid connection name (does not exist in database.yml): #{DbCharmer.env}/#{name}" 19 | end 20 | return # No need to establish connection - they do not want us to 21 | end 22 | 23 | # Pass connection name with config 24 | config[:connection_name] = name 25 | establish_connection(config) 26 | end 27 | 28 | #----------------------------------------------------------------------------------------------------------------- 29 | def hijack_connection! 30 | return if self.respond_to?(:connection_with_magic) 31 | class << self 32 | # Make sure we check our accessors before going to the default connection retrieval method 33 | def connection_with_magic 34 | db_charmer_remapped_connection || db_charmer_model_connection_proxy || connection_without_magic 35 | end 36 | alias_method_chain :connection, :magic 37 | 38 | def connection_pool_with_magic 39 | if connection.respond_to?(:abstract_connection_class) 40 | abstract_connection_class = connection.abstract_connection_class 41 | connection_handler.retrieve_connection_pool(abstract_connection_class) || connection_pool_without_magic 42 | else 43 | connection_pool_without_magic 44 | end 45 | end 46 | alias_method_chain :connection_pool, :magic 47 | end 48 | end 49 | 50 | #----------------------------------------------------------------------------------------------------------------- 51 | def coerce_to_connection_proxy(conn, should_exist = true) 52 | # Return nil if given no connection specification 53 | return nil if conn.nil? 54 | 55 | # For sharded proxies just use them as-is 56 | return conn if conn.respond_to?(:set_real_connection) 57 | 58 | # For connection proxies and objects that could be coerced into a proxy just call the coercion method 59 | return conn.db_charmer_connection_proxy if conn.respond_to?(:db_charmer_connection_proxy) 60 | 61 | # For plain AR connection adapters, just use them as-is 62 | return conn if conn.kind_of?(::ActiveRecord::ConnectionAdapters::AbstractAdapter) 63 | 64 | # For connection names, use connection factory to create new connections 65 | if conn.kind_of?(Symbol) || conn.kind_of?(String) 66 | return DbCharmer::ConnectionFactory.connect(conn, should_exist) 67 | end 68 | 69 | # For connection configs (hashes), create connections 70 | if conn.kind_of?(Hash) 71 | conn = conn.symbolize_keys 72 | raise ArgumentError, "Missing required :connection_name parameter" unless conn[:connection_name] 73 | return DbCharmer::ConnectionFactory.connect_to_db(conn[:connection_name], conn) 74 | end 75 | 76 | # Fails for unsupported connection types 77 | raise "Unsupported connection type: #{conn.class}" 78 | end 79 | 80 | #----------------------------------------------------------------------------------------------------------------- 81 | def switch_connection_to(conn, should_exist = true) 82 | new_conn = coerce_to_connection_proxy(conn, should_exist) 83 | 84 | if db_charmer_connection_proxy.respond_to?(:set_real_connection) 85 | db_charmer_connection_proxy.set_real_connection(new_conn) 86 | end 87 | 88 | self.db_charmer_connection_proxy = new_conn 89 | self.hijack_connection! 90 | end 91 | 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/db_magic.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module DbMagic 4 | 5 | def db_magic(opt = {}) 6 | # Make sure we could use our connections management here 7 | hijack_connection! 8 | 9 | # Should requested connections exist in the config? 10 | should_exist = opt.has_key?(:should_exist) ? opt[:should_exist] : DbCharmer.connections_should_exist? 11 | 12 | # Main connection management 13 | setup_connection_magic(opt[:connection], should_exist) 14 | 15 | # Set up slaves pool 16 | opt[:slaves] ||= [] 17 | opt[:slaves] = [ opt[:slaves] ].flatten 18 | opt[:slaves] << opt[:slave] if opt[:slave] 19 | 20 | # Forced reads are enabled for all models by default, could be disabled by the user 21 | forced_slave_reads = opt.has_key?(:force_slave_reads) ? opt[:force_slave_reads] : true 22 | 23 | # Setup all the slaves related magic if needed 24 | setup_slaves_magic(opt[:slaves], forced_slave_reads, should_exist) 25 | 26 | # Setup inheritance magic 27 | setup_children_magic(opt) 28 | 29 | # Setup sharding if needed 30 | if opt[:sharded] 31 | raise ArgumentError, "Can't use sharding on a model with slaves!" if opt[:slaves].any? 32 | setup_sharding_magic(opt[:sharded]) 33 | end 34 | end 35 | 36 | private 37 | 38 | def setup_children_magic(opt) 39 | self.db_charmer_opts = opt.clone 40 | 41 | unless self.respond_to?(:inherited_with_db_magic) 42 | class << self 43 | def inherited_with_db_magic(child) 44 | o = inherited_without_db_magic(child) 45 | child.db_magic(self.db_charmer_opts) 46 | o 47 | end 48 | alias_method_chain :inherited, :db_magic 49 | end 50 | end 51 | end 52 | 53 | def setup_sharding_magic(config) 54 | # Add sharding-specific methods 55 | self.extend(DbCharmer::ActiveRecord::Sharding) 56 | 57 | # Get configuration 58 | name = config[:sharded_connection] or raise ArgumentError, "No :sharded_connection!" 59 | # Assign sharded connection 60 | self.sharded_connection = DbCharmer::Sharding.sharded_connection(name) 61 | 62 | # Setup model default connection 63 | setup_connection_magic(sharded_connection.default_connection) 64 | end 65 | 66 | def setup_connection_magic(conn, should_exist = true) 67 | conn_proxy = coerce_to_connection_proxy(conn, should_exist) 68 | self.db_charmer_default_connection = conn_proxy 69 | switch_connection_to(conn_proxy, should_exist) 70 | end 71 | 72 | def setup_slaves_magic(slaves, force_slave_reads, should_exist = true) 73 | self.db_charmer_force_slave_reads = force_slave_reads 74 | 75 | # Initialize the slave connections list 76 | self.db_charmer_slaves = slaves.collect do |slave| 77 | coerce_to_connection_proxy(slave, should_exist) 78 | end 79 | return if db_charmer_slaves.empty? 80 | 81 | # Enable on_slave/on_master methods 82 | self.extend(DbCharmer::ActiveRecord::MultiDbProxy::MasterSlaveClassMethods) 83 | 84 | # Enable automatic master/slave queries routing (we have specialized versions on those modules for rails2/3) 85 | self.extend(DbCharmer::ActiveRecord::MasterSlaveRouting::ClassMethods) 86 | self.send(:include, DbCharmer::ActiveRecord::MasterSlaveRouting::InstanceMethods) 87 | end 88 | 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/migration/multi_db_migrations.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module Migration 4 | module MultiDbMigrations 5 | 6 | def self.append_features(base) 7 | return false if base < self 8 | super 9 | base.extend const_get("ClassMethods") if const_defined?("ClassMethods") 10 | 11 | base.class_eval do 12 | if DbCharmer.rails31? 13 | alias_method_chain :migrate, :db_wrapper 14 | else 15 | class << self 16 | alias_method_chain :migrate, :db_wrapper 17 | end 18 | end 19 | end 20 | end 21 | 22 | module ClassMethods 23 | @@multi_db_names = {} 24 | def multi_db_names 25 | @@multi_db_names[self.name] || @@multi_db_names['ActiveRecord::Migration'] 26 | end 27 | 28 | def multi_db_names=(names) 29 | @@multi_db_names[self.name] = names 30 | end 31 | 32 | unless DbCharmer.rails31? 33 | def migrate_with_db_wrapper(direction) 34 | if names = multi_db_names 35 | names.each do |multi_db_name| 36 | on_db(multi_db_name) do 37 | migrate_without_db_wrapper(direction) 38 | end 39 | end 40 | else 41 | migrate_without_db_wrapper(direction) 42 | end 43 | end 44 | 45 | def on_db(db_name) 46 | name = db_name.is_a?(Hash) ? db_name[:connection_name] : db_name.inspect 47 | announce "Switching connection to #{name}" 48 | # Switch connection 49 | old_proxy = ::ActiveRecord::Base.db_charmer_connection_proxy 50 | db_name = nil if db_name == :default 51 | ::ActiveRecord::Base.switch_connection_to(db_name, DbCharmer.connections_should_exist?) 52 | # Yield the block 53 | yield 54 | ensure 55 | # Switch it back 56 | ::ActiveRecord::Base.verify_active_connections! 57 | announce "Switching connection back" 58 | ::ActiveRecord::Base.switch_connection_to(old_proxy) 59 | end 60 | end 61 | 62 | def db_magic(opts = {}) 63 | # Collect connections from all possible options 64 | conns = [ opts[:connection], opts[:connections] ] 65 | conns << shard_connections(opts[:sharded_connection]) if opts[:sharded_connection] 66 | 67 | # Get a unique set of connections 68 | conns = conns.flatten.compact.uniq 69 | raise ArgumentError, "No connection name - no magic!" unless conns.any? 70 | 71 | # Save connections 72 | self.multi_db_names = conns 73 | end 74 | 75 | # Return a list of connections to shards in a sharded connection 76 | def shard_connections(conn_name) 77 | conn = DbCharmer::Sharding.sharded_connection(conn_name) 78 | conn.shard_connections 79 | end 80 | end 81 | 82 | def migrate_with_db_wrapper(direction) 83 | if names = self.class.multi_db_names 84 | names.each do |multi_db_name| 85 | on_db(multi_db_name) do 86 | migrate_without_db_wrapper(direction) 87 | end 88 | end 89 | else 90 | migrate_without_db_wrapper(direction) 91 | end 92 | end 93 | 94 | def record_on_db(db_name, block) 95 | recorder = ::ActiveRecord::Migration::CommandRecorder.new(DbCharmer::ConnectionFactory.connect(db_name)) 96 | old_recorder, @connection = @connection, recorder 97 | block.call 98 | old_recorder.record :on_db, [db_name, @connection] 99 | @connection = old_recorder 100 | end 101 | 102 | def replay_commands_on_db(name, commands) 103 | on_db(name) do 104 | commands.each do |cmd, args| 105 | send(cmd, *args) 106 | end 107 | end 108 | end 109 | 110 | def on_db(db_name, &block) 111 | if @connection.is_a?(::ActiveRecord::Migration::CommandRecorder) 112 | record_on_db(db_name, block) 113 | return 114 | end 115 | 116 | name = db_name.is_a?(Hash) ? db_name[:connection_name] : db_name.inspect 117 | announce "Switching connection to #{name}" 118 | # Switch connection 119 | old_connection, old_proxy = @connection, ::ActiveRecord::Base.db_charmer_connection_proxy 120 | db_name = nil if db_name == :default 121 | ::ActiveRecord::Base.switch_connection_to(db_name, DbCharmer.connections_should_exist?) 122 | # Yield the block 123 | ::ActiveRecord::Base.connection_pool.with_connection do |conn| 124 | @connection = conn 125 | yield 126 | end 127 | ensure 128 | @connection = old_connection 129 | # Switch it back 130 | ::ActiveRecord::Base.verify_active_connections! 131 | announce "Switching connection back" 132 | ::ActiveRecord::Base.switch_connection_to(old_proxy) 133 | end 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/multi_db_proxy.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module MultiDbProxy 4 | # Simple proxy class that switches connections and then proxies all the calls 5 | # This class is used to implement chained on_db calls 6 | class OnDbProxy < ActiveSupport::BasicObject 7 | # We need to do this because in Rails 2.3 BasicObject does not remove object_id method, which is stupid 8 | undef_method(:object_id) if instance_methods.member?('object_id') 9 | 10 | def initialize(proxy_target, slave) 11 | @proxy_target = proxy_target 12 | @slave = slave 13 | end 14 | 15 | private 16 | 17 | def method_missing(meth, *args, &block) 18 | # Switch connection and proxy the method call 19 | @proxy_target.on_db(@slave) do |proxy_target| 20 | res = proxy_target.__send__(meth, *args, &block) 21 | 22 | # If result is a scope/association, return a new proxy for it, otherwise return the result itself 23 | (res.proxy?) ? OnDbProxy.new(res, @slave) : res 24 | end 25 | end 26 | end 27 | 28 | module ClassMethods 29 | def on_db(con, proxy_target = nil) 30 | proxy_target ||= self 31 | 32 | # Chain call 33 | return OnDbProxy.new(proxy_target, con) unless block_given? 34 | 35 | # Block call 36 | begin 37 | self.db_charmer_connection_level += 1 38 | old_proxy = db_charmer_connection_proxy 39 | switch_connection_to(con, DbCharmer.connections_should_exist?) 40 | yield(proxy_target) 41 | ensure 42 | switch_connection_to(old_proxy) 43 | self.db_charmer_connection_level -= 1 44 | end 45 | end 46 | end 47 | 48 | module InstanceMethods 49 | def on_db(con, proxy_target = nil, &block) 50 | proxy_target ||= self 51 | self.class.on_db(con, proxy_target, &block) 52 | end 53 | end 54 | 55 | module MasterSlaveClassMethods 56 | def on_slave(con = nil, proxy_target = nil, &block) 57 | con ||= db_charmer_random_slave 58 | raise ArgumentError, "No slaves found in the class and no slave connection given" unless con 59 | on_db(con, proxy_target, &block) 60 | end 61 | 62 | def on_master(proxy_target = nil, &block) 63 | on_db(db_charmer_default_connection, proxy_target, &block) 64 | end 65 | 66 | def first_level_on_slave 67 | first_level = db_charmer_top_level_connection? && on_master.connection.open_transactions.zero? 68 | if first_level && db_charmer_force_slave_reads? && db_charmer_slaves.any? 69 | on_slave { yield } 70 | else 71 | yield 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/db_charmer/active_record/sharding.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module Sharding 4 | 5 | def self.extended(model) 6 | model.cattr_accessor(:sharded_connection) 7 | end 8 | 9 | def shard_for(key, proxy_target = nil, &block) 10 | raise ArgumentError, "No sharded connection configured!" unless sharded_connection 11 | conn = sharded_connection.sharder.shard_for_key(key) 12 | on_db(conn, proxy_target, &block) 13 | end 14 | 15 | # Run on default shard (if supported by the sharding method) 16 | def on_default_shard(proxy_target = nil, &block) 17 | raise ArgumentError, "No sharded connection configured!" unless sharded_connection 18 | 19 | if sharded_connection.support_default_shard? 20 | shard_for(:default, proxy_target, &block) 21 | else 22 | raise ArgumentError, "This model's sharding method does not support default shard" 23 | end 24 | end 25 | 26 | # Enumerate shards 27 | def on_each_shard(proxy_target = nil, &block) 28 | raise ArgumentError, "No sharded connection configured!" unless sharded_connection 29 | 30 | conns = sharded_connection.shard_connections 31 | raise ArgumentError, "This model's sharding method does not support shards enumeration" unless conns 32 | 33 | conns.each do |conn| 34 | on_db(conn, proxy_target, &block) 35 | end 36 | end 37 | 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/db_charmer/connection_factory.rb: -------------------------------------------------------------------------------- 1 | # 2 | # This class is used to automatically generate small abstract ActiveRecord classes 3 | # that would then be used as a source of database connections for DbCharmer magic. 4 | # This way we do not need to re-implement all the connection establishing code 5 | # that ActiveRecord already has and we make our code less dependant on Rails versions. 6 | # 7 | module DbCharmer 8 | module ConnectionFactory 9 | def self.connection_classes 10 | Thread.current[:db_charmer_generated_connection_classes] ||= {} 11 | end 12 | 13 | def self.connection_classes=(val) 14 | Thread.current[:db_charmer_generated_connection_classes] = val 15 | end 16 | 17 | def self.reset! 18 | self.connection_classes = {} 19 | end 20 | 21 | # Establishes connection or return an existing one from cache 22 | def self.connect(connection_name, should_exist = true) 23 | connection_name = connection_name.to_s 24 | connection_classes[connection_name] ||= establish_connection(connection_name, should_exist) 25 | end 26 | 27 | # Establishes connection or return an existing one from cache (not using AR database configs) 28 | def self.connect_to_db(connection_name, config) 29 | connection_name = connection_name.to_s 30 | connection_classes[connection_name] ||= establish_connection_to_db(connection_name, config) 31 | end 32 | 33 | # Establish connection with a specified name 34 | def self.establish_connection(connection_name, should_exist = true) 35 | abstract_class = generate_abstract_class(connection_name, should_exist) 36 | DbCharmer::ConnectionProxy.new(abstract_class, connection_name) 37 | end 38 | 39 | # Establish connection with a specified name (not using AR database configs) 40 | def self.establish_connection_to_db(connection_name, config) 41 | abstract_class = generate_abstract_class_for_db(connection_name, config) 42 | DbCharmer::ConnectionProxy.new(abstract_class, connection_name) 43 | end 44 | 45 | # Generate an abstract AR class with specified connection established 46 | def self.generate_abstract_class(connection_name, should_exist = true) 47 | # Generate class 48 | klass = generate_empty_abstract_ar_class(abstract_connection_class_name(connection_name)) 49 | 50 | # Establish connection 51 | klass.establish_real_connection_if_exists(connection_name.to_sym, !!should_exist) 52 | 53 | # Return the class 54 | return klass 55 | end 56 | 57 | # Generate an abstract AR class with specified connection established (not using AR database configs) 58 | def self.generate_abstract_class_for_db(connection_name, config) 59 | # Generate class 60 | klass = generate_empty_abstract_ar_class(abstract_connection_class_name(connection_name)) 61 | 62 | # Establish connection 63 | klass.establish_connection(config) 64 | 65 | # Return the class 66 | return klass 67 | end 68 | 69 | def self.generate_empty_abstract_ar_class(klass) 70 | # Define class 71 | module_eval "class #{klass} < ::ActiveRecord::Base; self.abstract_class = true; end" 72 | 73 | # Return class 74 | klass.constantize 75 | end 76 | 77 | # Generates unique names for our abstract AR classes 78 | def self.abstract_connection_class_name(connection_name) 79 | conn_name_klass = connection_name.to_s.gsub(/\W+/, '_').camelize 80 | thread = Thread.current.object_id.abs # need to make sure it is non-negative 81 | "::AutoGeneratedAbstractConnectionClass#{conn_name_klass}ForThread#{thread}" 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/db_charmer/connection_proxy.rb: -------------------------------------------------------------------------------- 1 | # Simple proxy that sends all method calls to a real database connection 2 | module DbCharmer 3 | class ConnectionProxy < ActiveSupport::BasicObject 4 | # We need to do this because in Rails 2.3 BasicObject does not remove object_id method, which is stupid 5 | undef_method(:object_id) if instance_methods.member?('object_id') 6 | 7 | # We use this to get a connection class from the proxy 8 | attr_accessor :abstract_connection_class 9 | 10 | def initialize(abstract_class, db_name) 11 | @abstract_connection_class = abstract_class 12 | @db_name = db_name 13 | end 14 | 15 | def db_charmer_connection_name 16 | @db_name 17 | end 18 | 19 | def db_charmer_connection_proxy 20 | self 21 | end 22 | 23 | def db_charmer_retrieve_connection 24 | @abstract_connection_class.retrieve_connection 25 | end 26 | 27 | def nil? 28 | false 29 | end 30 | 31 | #----------------------------------------------------------------------------------------------- 32 | RESPOND_TO_METHODS = [ 33 | :abstract_connection_class, 34 | :db_charmer_connection_name, 35 | :db_charmer_connection_proxy, 36 | :db_charmer_retrieve_connection, 37 | :nil? 38 | ].freeze 39 | 40 | # Short-circuit some of the methods for which we know there is a separate check in coercion code 41 | DOESNT_RESPOND_TO_METHODS = [ 42 | :set_real_connection 43 | ].freeze 44 | 45 | def respond_to?(method_name, include_all = false) 46 | return true if RESPOND_TO_METHODS.include?(method_name) 47 | return false if DOESNT_RESPOND_TO_METHODS.include?(method_name) 48 | db_charmer_retrieve_connection.respond_to?(method_name, include_all) 49 | end 50 | 51 | #----------------------------------------------------------------------------------------------- 52 | def method_missing(meth, *args, &block) 53 | db_charmer_retrieve_connection.send(meth, *args, &block) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/db_charmer/core_extensions.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | unless defined?(try) 3 | def try(method, *options, &block) 4 | send(method, *options, &block) 5 | end 6 | end 7 | 8 | # These methods are added to all objects so we could call proxy? on anything 9 | # and figure if an object is a proxy w/o hitting method_missing or respond_to? 10 | def self.proxy? 11 | false 12 | end 13 | 14 | def proxy? 15 | false 16 | end 17 | end 18 | 19 | class NilClass 20 | def try(*args) 21 | nil 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/db_charmer/force_slave_reads.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | def self.current_controller 3 | Thread.current[:db_charmer_current_controller] 4 | end 5 | 6 | def self.current_controller=(val) 7 | Thread.current[:db_charmer_current_controller] = val 8 | end 9 | 10 | #------------------------------------------------------------------------------------------------- 11 | def self.forced_slave_reads_setting 12 | Thread.current[:db_charmer_forced_slave_reads] 13 | end 14 | 15 | def self.forced_slave_reads_setting=(val) 16 | Thread.current[:db_charmer_forced_slave_reads] = val 17 | end 18 | 19 | #------------------------------------------------------------------------------------------------- 20 | def self.force_slave_reads? 21 | # If global force slave reads is requested, do it 22 | return true if Thread.current[:db_charmer_forced_slave_reads] 23 | 24 | # If not, try to use current controller to decide on this 25 | return false unless current_controller.respond_to?(:force_slave_reads?) 26 | 27 | slave_reads = current_controller.force_slave_reads? 28 | logger.debug("Using controller to figure out if slave reads should be forced: #{slave_reads}") 29 | return slave_reads 30 | end 31 | 32 | #------------------------------------------------------------------------------------------------- 33 | def self.with_controller(controller) 34 | raise ArgumentError, "No block given" unless block_given? 35 | logger.debug("Setting current controller for db_charmer: #{controller.class.name}") 36 | self.current_controller = controller 37 | yield 38 | ensure 39 | logger.debug('Clearing current controller for db_charmer') 40 | self.current_controller = nil 41 | end 42 | 43 | #------------------------------------------------------------------------------------------------- 44 | # Force all reads in a block of code to go to a slave 45 | def self.force_slave_reads 46 | raise ArgumentError, "No block given" unless block_given? 47 | old_forced_slave_reads = self.forced_slave_reads_setting 48 | begin 49 | self.forced_slave_reads_setting = true 50 | yield 51 | ensure 52 | self.forced_slave_reads_setting = old_forced_slave_reads 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/db_charmer/rails2/abstract_adapter/log_formatting.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module AbstractAdapter 3 | module LogFormatting 4 | 5 | def self.included(base) 6 | base.alias_method_chain :format_log_entry, :connection_name 7 | end 8 | 9 | def connection_name 10 | raise "Can't find connection configuration!" unless @config 11 | @config[:connection_name] 12 | end 13 | 14 | # Rails 2.X specific logging method 15 | def format_log_entry_with_connection_name(message, dump = nil) 16 | msg = connection_name ? "[#{connection_name}] " : '' 17 | msg = " \e[0;34;1m#{msg}\e[0m" if connection_name && ::ActiveRecord::Base.colorize_logging 18 | msg << format_log_entry_without_connection_name(message, dump) 19 | end 20 | 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/db_charmer/rails2/active_record/master_slave_routing.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module MasterSlaveRouting 4 | 5 | module ClassMethods 6 | SLAVE_METHODS = [ :find_by_sql, :count_by_sql, :calculate ] 7 | MASTER_METHODS = [ :update, :create, :delete, :destroy, :delete_all, :destroy_all, :update_all, :update_counters ] 8 | 9 | SLAVE_METHODS.each do |slave_method| 10 | class_eval <<-EOF, __FILE__, __LINE__ + 1 11 | def #{slave_method}(*args, &block) 12 | first_level_on_slave do 13 | super(*args, &block) 14 | end 15 | end 16 | EOF 17 | end 18 | 19 | MASTER_METHODS.each do |master_method| 20 | class_eval <<-EOF, __FILE__, __LINE__ + 1 21 | def #{master_method}(*args, &block) 22 | on_master do 23 | super(*args, &block) 24 | end 25 | end 26 | EOF 27 | end 28 | 29 | def find(*args, &block) 30 | options = args.last 31 | if options.is_a?(Hash) && options[:lock] 32 | on_master { super(*args, &block) } 33 | else 34 | super(*args, &block) 35 | end 36 | end 37 | end 38 | 39 | module InstanceMethods 40 | def reload(*args, &block) 41 | self.class.on_master do 42 | super(*args, &block) 43 | end 44 | end 45 | end 46 | 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/db_charmer/rails2/active_record/named_scope/scope_proxy.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module NamedScope 4 | module ScopeProxy 5 | 6 | def proxy? 7 | true 8 | end 9 | 10 | def on_db(con, proxy_target = nil, &block) 11 | proxy_target ||= self 12 | proxy_scope.on_db(con, proxy_target, &block) 13 | end 14 | 15 | def on_slave(con = nil, &block) 16 | proxy_scope.on_slave(con, self, &block) 17 | end 18 | 19 | def on_master(&block) 20 | proxy_scope.on_master(self, &block) 21 | end 22 | 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/db_charmer/rails3/abstract_adapter/connection_name.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module AbstractAdapter 3 | module ConnectionName 4 | 5 | # We use this proxy to push connection name down to instrumenters w/o monkey-patching the log method itself 6 | class InstrumenterDecorator < ActiveSupport::BasicObject 7 | def initialize(adapter, instrumenter) 8 | @adapter = adapter 9 | @instrumenter = instrumenter 10 | end 11 | 12 | def instrument(name, payload = {}, &block) 13 | payload[:connection_name] ||= @adapter.connection_name 14 | @instrumenter.instrument(name, payload, &block) 15 | end 16 | 17 | def method_missing(meth, *args, &block) 18 | @instrumenter.send(meth, *args, &block) 19 | end 20 | end 21 | 22 | def self.included(base) 23 | base.alias_method_chain :initialize, :connection_name 24 | end 25 | 26 | def connection_name 27 | raise "Can't find connection configuration!" unless @config 28 | @config[:connection_name] 29 | end 30 | 31 | def initialize_with_connection_name(*args) 32 | initialize_without_connection_name(*args) 33 | @instrumenter = InstrumenterDecorator.new(self, @instrumenter) 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/db_charmer/rails3/active_record/log_subscriber.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module LogSubscriber 4 | 5 | def self.included(base) 6 | base.send(:attr_accessor, :connection_name) 7 | base.alias_method_chain :sql, :connection_name 8 | base.alias_method_chain :debug, :connection_name 9 | end 10 | 11 | def sql_with_connection_name(event) 12 | self.connection_name = event.payload[:connection_name] 13 | sql_without_connection_name(event) 14 | end 15 | 16 | def debug_with_connection_name(msg) 17 | conn = connection_name ? color(" [#{connection_name}]", ActiveSupport::LogSubscriber::BLUE, true) : '' 18 | debug_without_connection_name(conn + msg) 19 | end 20 | 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/db_charmer/rails3/active_record/master_slave_routing.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module MasterSlaveRouting 4 | 5 | module ClassMethods 6 | SLAVE_METHODS = [ :find_by_sql, :count_by_sql ] 7 | MASTER_METHODS = [ ] # I don't know any methods in AR::Base that change data directly w/o going to the relation object 8 | 9 | SLAVE_METHODS.each do |slave_method| 10 | class_eval <<-EOF, __FILE__, __LINE__ + 1 11 | def #{slave_method}(*args, &block) 12 | first_level_on_slave do 13 | super(*args, &block) 14 | end 15 | end 16 | EOF 17 | end 18 | 19 | MASTER_METHODS.each do |master_method| 20 | class_eval <<-EOF, __FILE__, __LINE__ + 1 21 | def #{master_method}(*args, &block) 22 | on_master do 23 | super(*args, &block) 24 | end 25 | end 26 | EOF 27 | end 28 | end 29 | 30 | module InstanceMethods 31 | MASTER_METHODS = [ :reload ] 32 | 33 | MASTER_METHODS.each do |master_method| 34 | class_eval <<-EOF, __FILE__, __LINE__ + 1 35 | def #{master_method}(*args, &block) 36 | self.class.on_master do 37 | super(*args, &block) 38 | end 39 | end 40 | EOF 41 | end 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/db_charmer/rails3/active_record/relation/connection_routing.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module Relation 4 | module ConnectionRouting 5 | 6 | # All the methods that could be querying the database 7 | SLAVE_METHODS = [ :calculate, :exists? ] 8 | MASTER_METHODS = [ :delete, :delete_all, :destroy, :destroy_all, :reload, :update, :update_all ] 9 | ALL_METHODS = SLAVE_METHODS + MASTER_METHODS 10 | 11 | DB_CHARMER_ATTRIBUTES = [ :db_charmer_connection, :db_charmer_connection_is_forced, :db_charmer_enable_slaves ] 12 | 13 | # Define the default relation connection + override all the query methods here 14 | def self.included(base) 15 | init_attributes(base) 16 | init_routing(base) 17 | end 18 | 19 | # Define our attributes + spawn methods shit needs to be changed to make sure our accessors are copied over to the new instances 20 | def self.init_attributes(base) 21 | DB_CHARMER_ATTRIBUTES.each do |attr| 22 | base.send(:attr_accessor, attr) 23 | end 24 | 25 | # Override spawn methods 26 | base.alias_method_chain :except, :db_charmer 27 | base.alias_method_chain :only, :db_charmer 28 | end 29 | 30 | # Override all query methods 31 | def self.init_routing(base) 32 | ALL_METHODS.each do |meth| 33 | base.alias_method_chain meth, :db_charmer 34 | end 35 | 36 | # Special case: for normal selects we go to the slave, but for selects with a lock we should use master 37 | base.alias_method_chain :to_a, :db_charmer 38 | end 39 | 40 | # Copy db_charmer attributes in addition to what they're copying 41 | def except_with_db_charmer(*args) 42 | except_without_db_charmer(*args).tap do |result| 43 | copy_db_charmer_options(self, result) 44 | end 45 | end 46 | 47 | # Copy db_charmer attributes in addition to what they're copying 48 | def only_with_db_charmer(*args) 49 | only_without_db_charmer(*args).tap do |result| 50 | copy_db_charmer_options(self, result) 51 | end 52 | end 53 | 54 | # Copy our accessors from one instance to another 55 | def copy_db_charmer_options(src, dst) 56 | DB_CHARMER_ATTRIBUTES.each do |attr| 57 | dst.send("#{attr}=".to_sym, src.send(attr)) 58 | end 59 | end 60 | 61 | # Connection switching (changes the default relation connection) 62 | def on_db(con, &block) 63 | if block_given? 64 | @klass.on_db(con, &block) 65 | else 66 | clone.tap do |result| 67 | result.db_charmer_connection = con 68 | result.db_charmer_connection_is_forced = true 69 | end 70 | end 71 | end 72 | 73 | # Make sure we get the right connection here 74 | def connection 75 | @klass.on_db(db_charmer_connection).connection 76 | end 77 | 78 | # Selects preferred destination (master/slave/default) for a query 79 | def select_destination(method, recommendation = :default) 80 | # If this relation was created within a forced connection block (e.g Model.on_db(:foo).relation) 81 | # Then we should use that connection everywhere except cases when a model is slave-enabled 82 | # in those cases DML queries go to the master 83 | if db_charmer_connection_is_forced 84 | return :master if db_charmer_enable_slaves && MASTER_METHODS.member?(method) 85 | return :default 86 | end 87 | 88 | # If this relation is created from a slave-enabled model, let's do the routing if possible 89 | if db_charmer_enable_slaves 90 | return :slave if SLAVE_METHODS.member?(method) 91 | return :master if MASTER_METHODS.member?(method) 92 | else 93 | # Make sure we do not use recommended destination 94 | recommendation = :default 95 | end 96 | 97 | # If nothing else came up, let's use the default or recommended connection 98 | return recommendation 99 | end 100 | 101 | # Switch the model to default relation connection 102 | def switch_connection_for_method(method, recommendation = nil) 103 | # Choose where to send the query 104 | destination ||= select_destination(method, recommendation) 105 | 106 | # What method to use 107 | on_db_method = [ :on_db, db_charmer_connection ] 108 | on_db_method = :on_master if destination == :master 109 | on_db_method = :first_level_on_slave if destination == :slave 110 | 111 | # Perform the query 112 | @klass.send(*on_db_method) do 113 | yield 114 | end 115 | end 116 | 117 | # For normal selects we go to the slave, but for selects with a lock we should use master 118 | def to_a_with_db_charmer(*args, &block) 119 | preferred_destination = :slave 120 | preferred_destination = :master if lock_value 121 | 122 | switch_connection_for_method(:to_a, preferred_destination) do 123 | to_a_without_db_charmer(*args, &block) 124 | end 125 | end 126 | 127 | # Need this to mimick alias_method_chain name generation (exists? => exists_with_db_charmer?) 128 | def self.aliased_method_name(target, with) 129 | aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 130 | "#{aliased_target}_#{with}_db_charmer#{punctuation}" 131 | end 132 | 133 | # Override all the query methods here 134 | ALL_METHODS.each do |method| 135 | class_eval <<-EOF, __FILE__, __LINE__ + 1 136 | def #{aliased_method_name method, :with}(*args, &block) 137 | switch_connection_for_method(:#{method.to_s}) do 138 | #{aliased_method_name method, :without}(*args, &block) 139 | end 140 | end 141 | EOF 142 | end 143 | 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/db_charmer/rails3/active_record/relation_method.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module RelationMethod 4 | 5 | def self.extended(base) 6 | class << base 7 | alias_method_chain :relation, :db_charmer 8 | alias_method_chain :arel_engine, :db_charmer 9 | end 10 | end 11 | 12 | # Create a relation object and initialize its default connection 13 | def relation_with_db_charmer(*args, &block) 14 | relation_without_db_charmer(*args, &block).tap do |rel| 15 | rel.db_charmer_connection = self.connection 16 | rel.db_charmer_enable_slaves = self.db_charmer_slaves.any? 17 | rel.db_charmer_connection_is_forced = !db_charmer_top_level_connection? 18 | end 19 | end 20 | 21 | # Use the model itself an engine for Arel, do not fall back to AR::Base 22 | def arel_engine_with_db_charmer(*) 23 | self 24 | end 25 | 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/db_charmer/rails31/active_record/migration/command_recorder.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module Migration 4 | module CommandRecorder 5 | def invert_on_db(args) 6 | [:replay_commands_on_db, [args.first, args[1].inverse]] 7 | end 8 | end 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/db_charmer/rails31/active_record/preloader/association.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module Preloader 4 | module Association 5 | extend ActiveSupport::Concern 6 | included do 7 | alias_method_chain :build_scope, :db_magic 8 | end 9 | 10 | def build_scope_with_db_magic 11 | if model.db_charmer_top_level_connection? || reflection.options[:polymorphic] || 12 | model.db_charmer_default_connection != klass.db_charmer_default_connection 13 | build_scope_without_db_magic 14 | else 15 | build_scope_without_db_magic.on_db(model) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/db_charmer/rails31/active_record/preloader/has_and_belongs_to_many.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module ActiveRecord 3 | module Preloader 4 | module HasAndBelongsToMany 5 | extend ActiveSupport::Concern 6 | included do 7 | alias_method_chain :records_for, :db_magic 8 | end 9 | 10 | def records_for_with_db_magic(ids) 11 | if model.db_charmer_top_level_connection? || reflection.options[:polymorphic] || 12 | model.db_charmer_default_connection != klass.db_charmer_default_connection 13 | records_for_without_db_magic(ids) 14 | else 15 | klass.on_db(model) do 16 | records_for_without_db_magic(ids) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/db_charmer/railtie.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | class Railtie < Rails::Railtie 3 | 4 | rake_tasks do 5 | load "db_charmer/tasks/databases.rake" 6 | end 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/db_charmer/sharding.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module Sharding 3 | autoload :Connection, 'db_charmer/sharding/connection' 4 | autoload :StubConnection, 'db_charmer/sharding/stub_connection' 5 | autoload :Method, 'db_charmer/sharding/method' 6 | 7 | @@sharded_connections = {} 8 | 9 | def self.register_connection(config) 10 | name = config[:name] or raise ArgumentError, "No :name in connection!" 11 | @@sharded_connections[name] = DbCharmer::Sharding::Connection.new(config) 12 | end 13 | 14 | def self.sharded_connection(name) 15 | @@sharded_connections[name] or raise ArgumentError, "Invalid sharded connection name!" 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/db_charmer/sharding/connection.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module Sharding 3 | class Connection 4 | attr_accessor :config, :sharder 5 | 6 | def initialize(config) 7 | @config = config 8 | @sharder = self.instantiate_sharder 9 | end 10 | 11 | def instantiate_sharder 12 | raise ArgumentError, "No :method passed!" unless config[:method] 13 | sharder_class_name = "DbCharmer::Sharding::Method::#{config[:method].to_s.classify}" 14 | sharder_class = sharder_class_name.constantize 15 | sharder_class.new(config) 16 | end 17 | 18 | def shard_connections 19 | sharder.respond_to?(:shard_connections) ? sharder.shard_connections : nil 20 | end 21 | 22 | def support_default_shard? 23 | sharder.respond_to?(:support_default_shard?) && sharder.support_default_shard? 24 | end 25 | 26 | def default_connection 27 | @default_connection ||= DbCharmer::Sharding::StubConnection.new(self) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/db_charmer/sharding/method.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module Sharding 3 | module Method 4 | autoload :Range, 'db_charmer/sharding/method/range' 5 | autoload :HashMap, 'db_charmer/sharding/method/hash_map' 6 | autoload :DbBlockMap, 'db_charmer/sharding/method/db_block_map' 7 | autoload :DbBlockGroupMap, 'db_charmer/sharding/method/db_block_group_map' 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/db_charmer/sharding/method/db_block_map.rb: -------------------------------------------------------------------------------- 1 | # This is a more sophisticated sharding method based on a database-backed 2 | # blocks map that holds block-shard associations. It automatically 3 | # creates new blocks for new keys and assigns them to shards. 4 | # 5 | module DbCharmer 6 | module Sharding 7 | module Method 8 | class DbBlockMap 9 | # Sharder name 10 | attr_accessor :name 11 | 12 | # Mapping db connection 13 | attr_accessor :connection, :connection_name 14 | 15 | # Mapping table name 16 | attr_accessor :map_table 17 | 18 | # Shards table name 19 | attr_accessor :shards_table 20 | 21 | # Sharding keys block size 22 | attr_accessor :block_size 23 | 24 | def initialize(config) 25 | @name = config[:name] or raise(ArgumentError, "Missing required :name parameter!") 26 | @connection = DbCharmer::ConnectionFactory.connect(config[:connection], true) 27 | @block_size = (config[:block_size] || 10000).to_i 28 | 29 | @map_table = config[:map_table] or raise(ArgumentError, "Missing required :map_table parameter!") 30 | @shards_table = config[:shards_table] or raise(ArgumentError, "Missing required :shards_table parameter!") 31 | 32 | # Local caches 33 | @shard_info_cache = {} 34 | 35 | @blocks_cache = Rails.cache 36 | @blocks_cache_prefix = config[:blocks_cache_prefix] || "#{@name}_block:" 37 | end 38 | 39 | def shard_for_key(key) 40 | block = block_for_key(key) 41 | 42 | begin 43 | # Auto-allocate new blocks 44 | block ||= allocate_new_block_for_key(key) 45 | rescue ::ActiveRecord::StatementInvalid => e 46 | raise unless e.message.include?('Duplicate entry') 47 | block = block_for_key(key) 48 | end 49 | 50 | raise ArgumentError, "Invalid key value, no shards found for this key and could not create a new block!" unless block 51 | 52 | # Bail if no shard found 53 | shard_id = block['shard_id'].to_i 54 | shard_info = shard_info_by_id(shard_id) 55 | raise ArgumentError, "Invalid shard_id: #{shard_id}" unless shard_info 56 | 57 | # Get config 58 | shard_connection_config(shard_info) 59 | end 60 | 61 | class ShardInfo < ::ActiveRecord::Base 62 | validates_presence_of :db_host 63 | validates_presence_of :db_port 64 | validates_presence_of :db_user 65 | validates_presence_of :db_pass 66 | validates_presence_of :db_name 67 | end 68 | 69 | # Returns a block for a key 70 | def block_for_key(key, cache = true) 71 | # Cleanup the cache if asked to 72 | key_range = [ block_start_for_key(key), block_end_for_key(key) ] 73 | block_cache_key = "%d-%d" % key_range 74 | 75 | if cache 76 | cached_block = get_cached_block(block_cache_key) 77 | return cached_block if cached_block 78 | end 79 | 80 | # Fetch cached value or load from db 81 | block = begin 82 | sql = "SELECT * FROM #{map_table} WHERE start_id = #{key_range.first} AND end_id = #{key_range.last} LIMIT 1" 83 | connection.select_one(sql, 'Find a shard block') 84 | end 85 | 86 | set_cached_block(block_cache_key, block) 87 | 88 | return block 89 | end 90 | 91 | def get_cached_block(block_cache_key) 92 | @blocks_cache.read("#{@blocks_cache_prefix}#{block_cache_key}") 93 | end 94 | 95 | def set_cached_block(block_cache_key, block) 96 | @blocks_cache.write("#{@blocks_cache_prefix}#{block_cache_key}", block) 97 | end 98 | 99 | # Load shard info 100 | def shard_info_by_id(shard_id, cache = true) 101 | # Cleanup the cache if asked to 102 | @shard_info_cache[shard_id] = nil unless cache 103 | 104 | # Either load from cache or from db 105 | @shard_info_cache[shard_id] ||= begin 106 | prepare_shard_model 107 | ShardInfo.find_by_id(shard_id) 108 | end 109 | end 110 | 111 | def allocate_new_block_for_key(key) 112 | # Can't find any shards to use for blocks allocation! 113 | return nil unless shard = least_loaded_shard 114 | 115 | # Figure out block limits 116 | start_id = block_start_for_key(key) 117 | end_id = block_end_for_key(key) 118 | 119 | # Try to insert a new mapping (ignore duplicate key errors) 120 | sql = <<-SQL 121 | INSERT INTO #{map_table} 122 | SET start_id = #{start_id}, 123 | end_id = #{end_id}, 124 | shard_id = #{shard.id}, 125 | block_size = #{block_size}, 126 | created_at = NOW(), 127 | updated_at = NOW() 128 | SQL 129 | connection.execute(sql, "Allocate new block") 130 | 131 | # Increment the blocks counter on the shard 132 | ShardInfo.update_counters(shard.id, :blocks_count => +1) 133 | 134 | # Retry block search after creation 135 | block_for_key(key) 136 | end 137 | 138 | def least_loaded_shard 139 | prepare_shard_model 140 | 141 | # Select shard 142 | shard = ShardInfo.all(:conditions => { :enabled => true, :open => true }, :order => 'blocks_count ASC', :limit => 1).first 143 | raise "Can't find any shards to use for blocks allocation!" unless shard 144 | return shard 145 | end 146 | 147 | def block_start_for_key(key) 148 | block_size.to_i * (key.to_i / block_size.to_i) 149 | end 150 | 151 | def block_end_for_key(key) 152 | block_size.to_i + block_start_for_key(key) 153 | end 154 | 155 | # Create configuration (use mapping connection as a template) 156 | def shard_connection_config(shard) 157 | # Format connection name 158 | shard_name = "db_charmer_db_block_map_#{name}_shard_%05d" % shard.id 159 | 160 | # Here we get the mapping connection's configuration 161 | # They do not expose configs so we hack in and get the instance var 162 | # FIXME: Find a better way, maybe move config method to our ar extenstions 163 | connection.instance_variable_get(:@config).clone.merge( 164 | # Name for the connection factory 165 | :connection_name => shard_name, 166 | # Connection params 167 | :host => shard.db_host, 168 | :port => shard.db_port, 169 | :username => shard.db_user, 170 | :password => shard.db_pass, 171 | :database => shard.db_name 172 | ) 173 | end 174 | 175 | def create_shard(params) 176 | params = params.symbolize_keys 177 | [ :db_host, :db_port, :db_user, :db_pass, :db_name ].each do |arg| 178 | raise ArgumentError, "Missing required parameter: #{arg}" unless params[arg] 179 | end 180 | 181 | # Prepare model 182 | prepare_shard_model 183 | 184 | # Create the record 185 | ShardInfo.create! do |shard| 186 | shard.db_host = params[:db_host] 187 | shard.db_port = params[:db_port] 188 | shard.db_user = params[:db_user] 189 | shard.db_pass = params[:db_pass] 190 | shard.db_name = params[:db_name] 191 | end 192 | end 193 | 194 | def shard_connections 195 | # Find all shards 196 | prepare_shard_model 197 | shards = ShardInfo.all(:conditions => { :enabled => true }) 198 | # Map them to connections 199 | shards.map { |shard| shard_connection_config(shard) } 200 | end 201 | 202 | # Prepare model for working with our shards table 203 | def prepare_shard_model 204 | ShardInfo.table_name = shards_table 205 | ShardInfo.switch_connection_to(connection) 206 | end 207 | 208 | end 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/db_charmer/sharding/method/hash_map.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module Sharding 3 | module Method 4 | class HashMap 5 | attr_accessor :map 6 | 7 | def initialize(config) 8 | @map = config[:map].clone or raise ArgumentError, "No :map defined!" 9 | end 10 | 11 | def shard_for_key(key) 12 | res = map[key] || map[:default] 13 | raise ArgumentError, "Invalid key value, no shards found for this key!" unless res 14 | return res 15 | end 16 | 17 | def support_default_shard? 18 | map.has_key?(:default) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/db_charmer/sharding/method/range.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module Sharding 3 | module Method 4 | class Range 5 | attr_accessor :ranges 6 | 7 | def initialize(config) 8 | @ranges = config[:ranges] ? config[:ranges].clone : raise(ArgumentError, "No :ranges defined!") 9 | end 10 | 11 | def shard_for_key(key) 12 | return ranges[:default] if key == :default 13 | 14 | ranges.each do |range, shard| 15 | next if range == :default 16 | return shard if range.member?(key.to_i) 17 | end 18 | 19 | return ranges[:default] if ranges[:default] 20 | raise ArgumentError, "Invalid key value, no shards found for this key!" 21 | end 22 | 23 | def support_default_shard? 24 | ranges.has_key?(:default) 25 | end 26 | 27 | def shard_connections 28 | ranges.values.uniq 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/db_charmer/sharding/stub_connection.rb: -------------------------------------------------------------------------------- 1 | # This is a simple proxy class used as a default connection on sharded models 2 | # 3 | # The idea is to proxy all utility method calls to a real connection (set by 4 | # the +set_real_connection+ method when we switch shards) and fail on real 5 | # database querying calls forcing users to switch shard connections. 6 | # 7 | module DbCharmer 8 | module Sharding 9 | class StubConnection 10 | attr_accessor :sharded_connection 11 | 12 | def initialize(sharded_connection) 13 | @sharded_connection = sharded_connection 14 | @real_conn = nil 15 | end 16 | 17 | def set_real_connection(real_conn) 18 | @real_conn = real_conn 19 | end 20 | 21 | def db_charmer_connection_name 22 | "StubConnection" 23 | end 24 | 25 | def real_connection 26 | # Return memoized real connection 27 | return @real_conn if @real_conn 28 | 29 | # If sharded connection supports shards enumeration, get the first shard 30 | conn = sharded_connection.shard_connections.try(:first) 31 | 32 | # If we do not have real connection yet, try to use the default one (if it is supported by the sharder) 33 | conn ||= sharded_connection.sharder.shard_for_key(:default) if sharded_connection.support_default_shard? 34 | 35 | # Get connection proxy for our real connection 36 | return nil unless conn 37 | @real_conn = ::ActiveRecord::Base.coerce_to_connection_proxy(conn, DbCharmer.connections_should_exist?) 38 | end 39 | 40 | def respond_to?(method_name, include_all = false) 41 | return true if super 42 | return false if real_connection.object_id == self.object_id 43 | real_connection.respond_to?(method_name, include_all) 44 | end 45 | 46 | def method_missing(meth, *args, &block) 47 | # Fail on database statements 48 | if ::ActiveRecord::ConnectionAdapters::DatabaseStatements.instance_methods.member?(meth.to_s) 49 | raise ::ActiveRecord::ConnectionNotEstablished, "You have to switch connection on your model before using it!" 50 | end 51 | 52 | # Fail if no connection has been established yet 53 | unless real_connection 54 | raise ::ActiveRecord::ConnectionNotEstablished, "No real connection to proxy this method to!" 55 | end 56 | 57 | if real_connection.kind_of?(DbCharmer::Sharding::StubConnection) 58 | raise ::ActiveRecord::ConnectionNotEstablished, "You have to switch connection on your model before using it!" 59 | end 60 | 61 | # Proxy the call to our real connection target 62 | real_connection.__send__(meth, *args, &block) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/db_charmer/tasks/databases.rake: -------------------------------------------------------------------------------- 1 | namespace :db_charmer do 2 | namespace :create do 3 | desc 'Create all the local databases defined in config/database.yml' 4 | task :all => "db:load_config" do 5 | ::ActiveRecord::Base.configurations.each_value do |config| 6 | # Skip entries that don't have a database key, such as the first entry here: 7 | # 8 | # defaults: &defaults 9 | # adapter: mysql 10 | # username: root 11 | # password: 12 | # host: localhost 13 | # 14 | # development: 15 | # database: blog_development 16 | # <<: *defaults 17 | next unless config['database'] 18 | # Only connect to local databases 19 | local_database?(config) { create_core_and_sub_database(config) } 20 | end 21 | end 22 | end 23 | 24 | desc 'Create the databases defined in config/database.yml for the current RAILS_ENV' 25 | task :create => "db:load_config" do 26 | create_core_and_sub_database(ActiveRecord::Base.configurations[RAILS_ENV]) 27 | end 28 | 29 | def create_core_and_sub_database(config) 30 | create_database(config) 31 | config.each_value do | sub_config | 32 | next unless sub_config.is_a?(Hash) 33 | next unless sub_config['database'] 34 | create_database(sub_config) 35 | end 36 | end 37 | 38 | namespace :drop do 39 | desc 'Drops all the local databases defined in config/database.yml' 40 | task :all => "db:load_config" do 41 | ::ActiveRecord::Base.configurations.each_value do |config| 42 | # Skip entries that don't have a database key 43 | next unless config['database'] 44 | # Only connect to local databases 45 | local_database?(config) { drop_core_and_sub_database(config) } 46 | end 47 | end 48 | end 49 | 50 | desc 'Drops the database for the current RAILS_ENV' 51 | task :drop => "db:load_config" do 52 | config = ::ActiveRecord::Base.configurations[RAILS_ENV || 'development'] 53 | begin 54 | drop_core_and_sub_database(config) 55 | rescue Exception => e 56 | puts "Couldn't drop #{config['database']} : #{e.inspect}" 57 | end 58 | end 59 | 60 | 61 | def local_database?(config, &block) 62 | if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank? 63 | yield 64 | else 65 | puts "This task only modifies local databases. #{config['database']} is on a remote host." 66 | end 67 | end 68 | end 69 | 70 | def drop_core_and_sub_database(config) 71 | begin 72 | drop_database(config) 73 | rescue 74 | $stderr.puts "#{config['database']} not exists" 75 | end 76 | config.each_value do | sub_config | 77 | next unless sub_config.is_a?(Hash) 78 | next unless sub_config['database'] 79 | begin 80 | drop_database(sub_config) 81 | rescue 82 | $stderr.puts "#{config['database']} not exists" 83 | end 84 | end 85 | end 86 | 87 | -------------------------------------------------------------------------------- /lib/db_charmer/version.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | module Version 3 | MAJOR = 1 4 | MINOR = 9 5 | PATCH = 1 6 | BUILD = nil 7 | 8 | STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.') 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/db_charmer/with_remapped_databases.rb: -------------------------------------------------------------------------------- 1 | module DbCharmer 2 | def self.with_remapped_databases(mappings, &proc) 3 | old_mappings = ::ActiveRecord::Base.db_charmer_database_remappings 4 | begin 5 | ::ActiveRecord::Base.db_charmer_database_remappings = mappings 6 | if mappings[:master] || mappings['master'] 7 | with_all_hijacked(&proc) 8 | else 9 | proc.call 10 | end 11 | ensure 12 | ::ActiveRecord::Base.db_charmer_database_remappings = old_mappings 13 | end 14 | end 15 | 16 | def self.hijack_new_classes? 17 | !! Thread.current[:db_charmer_hijack_new_classes] 18 | end 19 | 20 | private 21 | 22 | def self.with_all_hijacked 23 | old_hijack_new_classes = Thread.current[:db_charmer_hijack_new_classes] 24 | begin 25 | Thread.current[:db_charmer_hijack_new_classes] = true 26 | subclasses_method = DbCharmer.rails3? ? :descendants : :subclasses 27 | ::ActiveRecord::Base.send(subclasses_method).each do |subclass| 28 | subclass.hijack_connection! 29 | end 30 | yield 31 | ensure 32 | Thread.current[:db_charmer_hijack_new_classes] = old_hijack_new_classes 33 | end 34 | end 35 | end 36 | 37 | #--------------------------------------------------------------------------------------------------- 38 | # Hijack connection on all new AR classes when we're in a block with main AR connection remapped 39 | class ActiveRecord::Base 40 | class << self 41 | def inherited_with_hijacking(subclass) 42 | out = inherited_without_hijacking(subclass) 43 | hijack_connection! if DbCharmer.hijack_new_classes? 44 | out 45 | end 46 | 47 | alias_method_chain :inherited, :hijacking 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test-project-2.x/.gitignore: -------------------------------------------------------------------------------- 1 | ../test-project/.gitignore -------------------------------------------------------------------------------- /test-project-2.x/.rspec: -------------------------------------------------------------------------------- 1 | ../test-project/.rspec -------------------------------------------------------------------------------- /test-project-2.x/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rails', '2.3.18' 4 | 5 | gem 'rake', '0.9.2.2' 6 | gem 'mysql' 7 | 8 | gem 'rspec', '1.3.2' 9 | gem 'rspec-rails', '1.3.4' 10 | 11 | # Load DbCharmer as a gem 12 | gem 'db-charmer', :path => '..', :require => 'db_charmer' 13 | -------------------------------------------------------------------------------- /test-project-2.x/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require(File.join(File.dirname(__FILE__), 'config', 'boot')) 5 | 6 | require 'rake' 7 | require 'rake/testtask' 8 | require 'rake/rdoctask' 9 | 10 | require 'tasks/rails' 11 | -------------------------------------------------------------------------------- /test-project-2.x/app: -------------------------------------------------------------------------------- 1 | ../test-project/app -------------------------------------------------------------------------------- /test-project-2.x/config/boot.rb: -------------------------------------------------------------------------------- 1 | # We only have test environment here 2 | ENV['RAILS_ENV'] = 'test' 3 | 4 | # Don't change this file! 5 | # Configure your app in config/environment.rb and config/environments/*.rb 6 | 7 | RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) 8 | 9 | module Rails 10 | class << self 11 | def boot! 12 | unless booted? 13 | preinitialize 14 | pick_boot.run 15 | end 16 | end 17 | 18 | def booted? 19 | defined? Rails::Initializer 20 | end 21 | 22 | def pick_boot 23 | (vendor_rails? ? VendorBoot : GemBoot).new 24 | end 25 | 26 | def vendor_rails? 27 | File.exist?("#{RAILS_ROOT}/vendor/rails") 28 | end 29 | 30 | def preinitialize 31 | load(preinitializer_path) if File.exist?(preinitializer_path) 32 | end 33 | 34 | def preinitializer_path 35 | "#{RAILS_ROOT}/config/preinitializer.rb" 36 | end 37 | end 38 | 39 | class Boot 40 | def run 41 | load_initializer 42 | 43 | Rails::Initializer.class_eval do 44 | def load_gems 45 | @bundler_loaded ||= Bundler.require :default, Rails.env 46 | end 47 | end 48 | 49 | Rails::Initializer.run(:set_load_path) 50 | end 51 | end 52 | 53 | class VendorBoot < Boot 54 | def load_initializer 55 | require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" 56 | Rails::Initializer.run(:install_gem_spec_stubs) 57 | Rails::GemDependency.add_frozen_gem_path 58 | end 59 | end 60 | 61 | class GemBoot < Boot 62 | def load_initializer 63 | self.class.load_rubygems 64 | load_rails_gem 65 | require 'initializer' 66 | end 67 | 68 | def load_rails_gem 69 | if version = self.class.gem_version 70 | gem 'rails', version 71 | else 72 | gem 'rails' 73 | end 74 | rescue Gem::LoadError => load_error 75 | $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) 76 | exit 1 77 | end 78 | 79 | class << self 80 | def rubygems_version 81 | Gem::RubyGemsVersion rescue nil 82 | end 83 | 84 | def gem_version 85 | if defined? RAILS_GEM_VERSION 86 | RAILS_GEM_VERSION 87 | elsif ENV.include?('RAILS_GEM_VERSION') 88 | ENV['RAILS_GEM_VERSION'] 89 | else 90 | parse_gem_version(read_environment_rb) 91 | end 92 | end 93 | 94 | def load_rubygems 95 | require 'rubygems' 96 | min_version = '1.3.1' 97 | unless rubygems_version >= min_version 98 | $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) 99 | exit 1 100 | end 101 | 102 | rescue LoadError 103 | $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) 104 | exit 1 105 | end 106 | 107 | def parse_gem_version(text) 108 | $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ 109 | end 110 | 111 | private 112 | def read_environment_rb 113 | File.read("#{RAILS_ROOT}/config/environment.rb") 114 | end 115 | end 116 | end 117 | end 118 | 119 | # All that for this: 120 | Rails.boot! 121 | -------------------------------------------------------------------------------- /test-project-2.x/config/database.yml.example: -------------------------------------------------------------------------------- 1 | common: &common 2 | adapter: mysql 3 | encoding: utf8 4 | reconnect: false 5 | pool: 1 6 | username: root 7 | password: 8 | 9 | #---------------------------------------------------------------- 10 | test: 11 | <<: *common 12 | database: db_charmer_sandbox_test 13 | 14 | # logs database 15 | logs: 16 | <<: *common 17 | database: db_charmer_logs_test 18 | 19 | # slave database 20 | slave01: 21 | <<: *common 22 | username: db_charmer_ro 23 | database: db_charmer_sandbox_test 24 | 25 | user_master: 26 | <<: *common 27 | database: db_charmer_sandbox_test 28 | 29 | # shard mapping db 30 | social_shard_info: 31 | <<: *common 32 | database: db_charmer_sandbox_test 33 | 34 | # for migrations only 35 | social_shard01: 36 | <<: *common 37 | database: db_charmer_events_test_shard01 38 | 39 | # for migrations only 40 | social_shard02: 41 | <<: *common 42 | database: db_charmer_events_test_shard02 43 | 44 | #---------------------------------------------------------------- 45 | test22: 46 | <<: *common 47 | database: db_charmer_sandbox22_test 48 | 49 | # logs database 50 | logs: 51 | <<: *common 52 | database: db_charmer_logs22_test 53 | 54 | # slave database 55 | slave01: 56 | <<: *common 57 | username: db_charmer_ro 58 | database: db_charmer_sandbox22_test 59 | 60 | user_master: 61 | <<: *common 62 | database: db_charmer_sandbox22_test 63 | 64 | # shard mapping db 65 | social_shard_info: 66 | <<: *common 67 | database: db_charmer_sandbox22_test 68 | 69 | # for migrations only 70 | social_shard01: 71 | <<: *common 72 | database: db_charmer_events22_test_shard01 73 | 74 | # for migrations only 75 | social_shard02: 76 | <<: *common 77 | database: db_charmer_events22_test_shard02 78 | -------------------------------------------------------------------------------- /test-project-2.x/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Specifies gem version of Rails to use when vendor/rails is not present 2 | RAILS_GEM_VERSION = '2.3.18' unless defined? RAILS_GEM_VERSION 3 | 4 | # Bootstrap the Rails environment, frameworks, and default configuration 5 | require File.join(File.dirname(__FILE__), 'boot') 6 | 7 | Rails::Initializer.run do |config| 8 | config.time_zone = 'UTC' 9 | end 10 | 11 | -------------------------------------------------------------------------------- /test-project-2.x/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | config.cache_classes = true 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.action_controller.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | config.action_view.cache_template_loading = true 16 | 17 | # Disable request forgery protection in test environment 18 | config.action_controller.allow_forgery_protection = false 19 | 20 | # Tell Action Mailer not to deliver emails to the real world. 21 | # The :test delivery method accumulates sent emails in the 22 | # ActionMailer::Base.deliveries array. 23 | config.action_mailer.delivery_method = :test 24 | 25 | # Use SQL instead of Active Record's schema dumper when creating the test database. 26 | # This is necessary if your schema can't be completely dumped by the schema dumper, 27 | # like if you have constraints or database-specific column types 28 | # config.active_record.schema_format = :sql 29 | -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying do debug a problem that might steem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/db_charmer.rb: -------------------------------------------------------------------------------- 1 | DbCharmer.connections_should_exist = false # Since we are not in production 2 | DbCharmer.enable_controller_magic! -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/new_rails_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # These settings change the behavior of Rails 2 apps and will be defaults 4 | # for Rails 3. You can remove this initializer when Rails 3 is released. 5 | 6 | if defined?(ActiveRecord) 7 | # Include Active Record class name as root for JSON serialized output. 8 | ActiveRecord::Base.include_root_in_json = true 9 | 10 | # Store the full class name (including module namespace) in STI type column. 11 | ActiveRecord::Base.store_full_sti_class = true 12 | end 13 | 14 | # Use ISO 8601 format for JSON serialized times and dates. 15 | ActiveSupport.use_standard_json_time_format = true 16 | 17 | # Don't escape HTML entities in JSON, leave that for the #json_escape helper. 18 | # if you're including raw json in an HTML page. 19 | ActiveSupport.escape_html_entities_in_json = false -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying cookie session data integrity. 4 | # If you change this key, all old sessions will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | ActionController::Base.session = { 8 | :key => '_db_charmer_sandbox_session', 9 | :secret => '9b67feed7aa8a2741d9f0ac6efde543d726f7a017c8a635346be733f287fd479fbd8521c1e8a06e91af7920de1fb50b942bdf24b6ecee1569ed947c13f6697af' 10 | } 11 | 12 | # Use the database for sessions instead of the cookie-based default, 13 | # which shouldn't be used to store highly confidential information 14 | # (create the session table with "rake db:sessions:create") 15 | # ActionController::Base.session_store = :active_record_store 16 | -------------------------------------------------------------------------------- /test-project-2.x/config/initializers/sharding.rb: -------------------------------------------------------------------------------- 1 | # Range-based shards for testing 2 | 3 | TEXTS_SHARDING_RANGES = { 4 | 0...100 => :shard1, 5 | 100..200 => :shard2, 6 | :default => :shard3 7 | } 8 | 9 | DbCharmer::Sharding.register_connection( 10 | :name => :texts, 11 | :method => :range, 12 | :ranges => TEXTS_SHARDING_RANGES 13 | ) 14 | 15 | #------------------------------------------------ 16 | # Db blocks map sharding for testing 17 | 18 | SOCIAL_SHARDING = DbCharmer::Sharding.register_connection( 19 | :name => :social, 20 | :method => :db_block_map, 21 | :block_size => 10, 22 | :map_table => :event_shards_map, 23 | :shards_table => :event_shards_info, 24 | :connection => :social_shard_info 25 | ) 26 | -------------------------------------------------------------------------------- /test-project-2.x/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" -------------------------------------------------------------------------------- /test-project-2.x/config/preinitializer.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "rubygems" 3 | require "bundler" 4 | rescue LoadError 5 | raise "Could not load the bundler gem. Install it with `gem install bundler`." 6 | end 7 | 8 | if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24") 9 | raise RuntimeError, "Your bundler version is too old for Rails 2.3." + 10 | "Run `gem install bundler` to upgrade." 11 | end 12 | 13 | begin 14 | # Set up load paths for all bundled gems 15 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__) 16 | Bundler.setup 17 | rescue Bundler::GemNotFound 18 | raise RuntimeError, "Bundler couldn't find some gems." + 19 | "Did you run `bundle install`?" 20 | end 21 | -------------------------------------------------------------------------------- /test-project-2.x/config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | # Resource routes 3 | map.resources :posts 4 | map.resources :cars 5 | 6 | # Install the default routes as the lowest priority. 7 | # Note: These default routes make all actions in every controller accessible via GET requests. You should 8 | # consider removing or commenting them out if you're using named routes and resources. 9 | map.connect ':controller/:action/:id' 10 | map.connect ':controller/:action/:id.:format' 11 | end 12 | -------------------------------------------------------------------------------- /test-project-2.x/db: -------------------------------------------------------------------------------- 1 | ../test-project/db -------------------------------------------------------------------------------- /test-project-2.x/script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/console' 4 | -------------------------------------------------------------------------------- /test-project-2.x/spec/controllers: -------------------------------------------------------------------------------- 1 | ../../test-project/spec/controllers -------------------------------------------------------------------------------- /test-project-2.x/spec/fixtures: -------------------------------------------------------------------------------- 1 | ../../test-project/spec/fixtures -------------------------------------------------------------------------------- /test-project-2.x/spec/models: -------------------------------------------------------------------------------- 1 | ../../test-project/spec/models -------------------------------------------------------------------------------- /test-project-2.x/spec/sharding: -------------------------------------------------------------------------------- 1 | ../../test-project/spec/sharding -------------------------------------------------------------------------------- /test-project-2.x/spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --format specdoc 3 | -------------------------------------------------------------------------------- /test-project-2.x/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec' 2 | # from the project root directory. 3 | ENV["RAILS_ENV"] = 'test' 4 | require File.expand_path(File.join(File.dirname(__FILE__),'..','config','environment')) 5 | require 'spec/autorun' 6 | require 'spec/rails' 7 | 8 | # Requires supporting files with custom matchers and macros, etc, 9 | # in ./support/ and its subdirectories. 10 | Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f} 11 | 12 | Spec::Runner.configure do |config| 13 | # If you're not using ActiveRecord you should remove these 14 | # lines, delete config/database.yml and disable :active_record 15 | # in your config/boot.rb 16 | config.use_transactional_fixtures = false 17 | config.use_instantiated_fixtures = false 18 | config.fixture_path = RAILS_ROOT + '/spec/fixtures/' 19 | 20 | # == Fixtures 21 | # 22 | # You can declare fixtures for each example_group like this: 23 | # describe "...." do 24 | # fixtures :table_a, :table_b 25 | # 26 | # Alternatively, if you prefer to declare them only once, you can 27 | # do so right here. Just uncomment the next line and replace the fixture 28 | # names with your fixtures. 29 | # 30 | # config.global_fixtures = :table_a, :table_b 31 | # 32 | # If you declare global fixtures, be aware that they will be declared 33 | # for all of your examples, even those that don't use them. 34 | # 35 | # You can also declare which fixtures to use (for example fixtures for test/fixtures): 36 | # 37 | # config.fixture_path = RAILS_ROOT + '/spec/fixtures/' 38 | # 39 | # == Mock Framework 40 | # 41 | # RSpec uses its own mocking framework by default. If you prefer to 42 | # use mocha, flexmock or RR, uncomment the appropriate line: 43 | # 44 | # config.mock_with :mocha 45 | # config.mock_with :flexmock 46 | # config.mock_with :rr 47 | # 48 | # == Notes 49 | # 50 | # For more information take a look at Spec::Runner::Configuration and Spec::Runner 51 | end 52 | -------------------------------------------------------------------------------- /test-project-2.x/spec/support: -------------------------------------------------------------------------------- 1 | ../../test-project/spec/support -------------------------------------------------------------------------------- /test-project-2.x/spec/unit: -------------------------------------------------------------------------------- 1 | ../../test-project/spec/unit -------------------------------------------------------------------------------- /test-project/.gitignore: -------------------------------------------------------------------------------- 1 | log/*.log 2 | db/schema.* 3 | .idea 4 | TAGS 5 | config/database.yml 6 | .bundle 7 | tmp 8 | .DS_Store 9 | vendor 10 | Gemfile.lock 11 | doc 12 | -------------------------------------------------------------------------------- /test-project/.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | -------------------------------------------------------------------------------- /test-project/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rake', "0.9.2.2" 4 | gem 'mysql', "2.8.1" 5 | 6 | gem 'rspec', '< 3.0' 7 | gem 'rspec-core', '< 3.0' 8 | gem 'rspec-rails', '< 3.0' 9 | 10 | # Load DbCharmer as a gem 11 | if ENV['DB_CHARMER_GEM'].to_s == '' 12 | gem_path = File.expand_path(File.dirname(File.dirname(__FILE__))) 13 | puts "Using on-disk db-charmer code from '#{gem_path}'..." 14 | gem 'db-charmer', :path => gem_path, :require => 'db_charmer' 15 | else 16 | puts "Using db-charmer gem: #{ENV['DB_CHARMER_GEM']}..." 17 | gem 'db-charmer', ENV['DB_CHARMER_GEM'], :require => 'db_charmer' 18 | end 19 | 20 | # Detect Rails version we need to use 21 | rails_version_file = File.expand_path("../.rails-version", __FILE__) 22 | version = File.exists?(rails_version_file) && File.read(rails_version_file).chomp 23 | version ||= ENV['RAILS_VERSION'] 24 | version ||= '3-2-stable' 25 | 26 | # Require gems for selected rails version 27 | case version 28 | when /master/ 29 | gem "rails", :git => "git://github.com/rails/rails.git" 30 | gem "arel", :git => "git://github.com/rails/arel.git" 31 | gem "journey", :git => "git://github.com/rails/journey.git" 32 | when /3-0-stable/ 33 | gem "rails", :git => "git://github.com/rails/rails.git", :branch => "3-0-stable" 34 | gem "arel", :git => "git://github.com/rails/arel.git", :branch => "2-0-stable" 35 | when /3-1-stable/ 36 | gem "rails", :git => "git://github.com/rails/rails.git", :branch => "3-1-stable" 37 | when /3-2-stable/ 38 | gem "rails", :git => "git://github.com/rails/rails.git", :branch => "3-2-stable" 39 | else 40 | gem "rails", version 41 | end 42 | -------------------------------------------------------------------------------- /test-project/Rakefile: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'test' 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path('../config/application', __FILE__) 7 | require 'rake' 8 | 9 | DbCharmerSandbox::Application.load_tasks 10 | -------------------------------------------------------------------------------- /test-project/TODO: -------------------------------------------------------------------------------- 1 | Functionality: 2 | - Add a controller wrapper to force all queries to the master (thanks mascohism for the idea) 3 | 4 | Docs: 5 | - Document (make more obvious) multi-db migrations code with migration_connections_should_exist 6 | - Document the fact, that all queries in a transaction go to the master 7 | -------------------------------------------------------------------------------- /test-project/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /test-project/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | force_slave_reads :only => [ :index, :show, :new ], :except => :new 3 | 4 | # We'll use this to make sure count query would be sent to a proper server 5 | before_filter do 6 | Post.count 7 | end 8 | 9 | def index 10 | @posts = Post.all 11 | end 12 | 13 | def show 14 | @post = Post.find(params[:id]) 15 | end 16 | 17 | def new 18 | @post = Post.new 19 | end 20 | 21 | def create 22 | post = Post.create!(params[:post]) 23 | redirect_to(post_url(post)) 24 | end 25 | 26 | def destroy 27 | Post.delete(params[:id]) 28 | redirect_to(:action => :index) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test-project/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test-project/app/models/avatar.rb: -------------------------------------------------------------------------------- 1 | class Avatar < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /test-project/app/models/car.rb: -------------------------------------------------------------------------------- 1 | class Car < ActiveRecord::Base 2 | db_magic :slave => :slave01 3 | end 4 | -------------------------------------------------------------------------------- /test-project/app/models/categories_posts.rb: -------------------------------------------------------------------------------- 1 | class CategoriesPosts < ActiveRecord::Base 2 | belongs_to :category 3 | belongs_to :post 4 | end 5 | -------------------------------------------------------------------------------- /test-project/app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ActiveRecord::Base 2 | has_and_belongs_to_many :posts 3 | end 4 | -------------------------------------------------------------------------------- /test-project/app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | belongs_to :commentable, :polymorphic => true 3 | end 4 | -------------------------------------------------------------------------------- /test-project/app/models/event.rb: -------------------------------------------------------------------------------- 1 | class Event < ActiveRecord::Base 2 | self.table_name = :timeline_events 3 | 4 | db_magic :sharded => { 5 | :key => :to_uid, 6 | :sharded_connection => :social 7 | } 8 | end 9 | -------------------------------------------------------------------------------- /test-project/app/models/ford.rb: -------------------------------------------------------------------------------- 1 | class Ford < Car 2 | end -------------------------------------------------------------------------------- /test-project/app/models/house.rb: -------------------------------------------------------------------------------- 1 | class House < ActiveRecord::Base 2 | db_magic :slave => :slave01, :force_slave_reads => false 3 | end 4 | -------------------------------------------------------------------------------- /test-project/app/models/log_record.rb: -------------------------------------------------------------------------------- 1 | class LogRecord < ActiveRecord::Base 2 | db_magic :connection => :logs 3 | belongs_to :user 4 | end 5 | -------------------------------------------------------------------------------- /test-project/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | DB_MAGIC_DEFAULT_PARAMS = { :slave => :slave01, :force_slave_reads => false } 3 | db_magic DB_MAGIC_DEFAULT_PARAMS 4 | 5 | belongs_to :user 6 | has_and_belongs_to_many :categories 7 | 8 | def self.define_scope(*args, &block) 9 | if DbCharmer.rails3? 10 | scope(*args, &block) 11 | else 12 | named_scope(*args, &block) 13 | end 14 | end 15 | 16 | define_scope :windows_posts, :conditions => "title like '%win%'" 17 | define_scope :dummy_scope, :conditions => '1' 18 | end 19 | -------------------------------------------------------------------------------- /test-project/app/models/range_sharded_model.rb: -------------------------------------------------------------------------------- 1 | class RangeShardedModel < ActiveRecord::Base 2 | db_magic :sharded => { 3 | :key => :id, 4 | :sharded_connection => :texts 5 | } 6 | end 7 | 8 | -------------------------------------------------------------------------------- /test-project/app/models/toyota.rb: -------------------------------------------------------------------------------- 1 | class Toyota < Car 2 | end -------------------------------------------------------------------------------- /test-project/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :posts 3 | has_many :log_records 4 | has_one :avatar 5 | end 6 | -------------------------------------------------------------------------------- /test-project/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DbCharmerSandbox 5 | <%= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag :defaults %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-project/app/views/posts/index.html.erb: -------------------------------------------------------------------------------- 1 |

Posts

2 |
3 | <% @posts.each do |post| %> 4 |

5 | Post #<%= post.id %>
6 |

<%= post.inspect %>
7 |

8 | <% end %> 9 | -------------------------------------------------------------------------------- /test-project/app/views/posts/new.html.erb: -------------------------------------------------------------------------------- 1 |

Posts#new

2 |

Find me in app/views/posts/new.html.erb

3 | -------------------------------------------------------------------------------- /test-project/app/views/posts/show.html.erb: -------------------------------------------------------------------------------- 1 |

Posts#show

2 |

Find me in app/views/posts/show.html.erb

3 | -------------------------------------------------------------------------------- /test-project/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # If you have a Gemfile, require the gems listed there, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(:default, Rails.env) if defined?(Bundler) 8 | 9 | module DbCharmerSandbox 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Custom directories with classes and modules you want to be autoloadable. 16 | # config.autoload_paths += %W(#{config.root}/extras) 17 | 18 | # Only load the plugins named here, in the order given (default is alphabetical). 19 | # :all can be used as a placeholder for all plugins not explicitly named. 20 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 21 | 22 | # Activate observers that should always be running. 23 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 24 | 25 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 26 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 27 | # config.time_zone = 'Central Time (US & Canada)' 28 | 29 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 30 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 31 | # config.i18n.default_locale = :de 32 | 33 | # JavaScript files you want as :defaults (application.js is always included). 34 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 35 | 36 | # Configure the default encoding used in templates for Ruby 1.9. 37 | config.encoding = "utf-8" 38 | 39 | # Configure sensitive parameters which will be filtered from the log file. 40 | config.filter_parameters += [:password] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test-project/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 | -------------------------------------------------------------------------------- /test-project/config/database.yml.example: -------------------------------------------------------------------------------- 1 | common: &common 2 | adapter: mysql 3 | encoding: utf8 4 | reconnect: false 5 | pool: 10 6 | username: root 7 | password: 8 | 9 | #---------------------------------------------------------------- 10 | test: 11 | <<: *common 12 | database: db_charmer_sandbox_test 13 | 14 | # logs database 15 | logs: 16 | <<: *common 17 | database: db_charmer_logs_test 18 | 19 | # slave database 20 | slave01: 21 | <<: *common 22 | username: db_charmer_ro 23 | database: db_charmer_sandbox_test 24 | 25 | user_master: 26 | <<: *common 27 | database: db_charmer_sandbox_test 28 | 29 | # shard mapping db 30 | social_shard_info: 31 | <<: *common 32 | database: db_charmer_sandbox_test 33 | 34 | # for migrations only 35 | social_shard01: 36 | <<: *common 37 | database: db_charmer_events_test_shard01 38 | 39 | # for migrations only 40 | social_shard02: 41 | <<: *common 42 | database: db_charmer_events_test_shard02 43 | 44 | #---------------------------------------------------------------- 45 | test22: 46 | <<: *common 47 | database: db_charmer_sandbox22_test 48 | 49 | # logs database 50 | logs: 51 | <<: *common 52 | database: db_charmer_logs22_test 53 | 54 | # slave database 55 | slave01: 56 | <<: *common 57 | username: db_charmer_ro 58 | database: db_charmer_sandbox22_test 59 | 60 | user_master: 61 | <<: *common 62 | database: db_charmer_sandbox22_test 63 | 64 | # shard mapping db 65 | social_shard_info: 66 | <<: *common 67 | database: db_charmer_sandbox22_test 68 | 69 | # for migrations only 70 | social_shard01: 71 | <<: *common 72 | database: db_charmer_events22_test_shard01 73 | 74 | # for migrations only 75 | social_shard02: 76 | <<: *common 77 | database: db_charmer_events22_test_shard02 78 | -------------------------------------------------------------------------------- /test-project/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | DbCharmerSandbox::Application.initialize! 6 | -------------------------------------------------------------------------------- /test-project/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | DbCharmerSandbox::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 | # Log error messages when you accidentally call methods on nil. 11 | config.whiny_nils = true 12 | 13 | # Show full error reports and disable caching 14 | config.consider_all_requests_local = true 15 | config.action_controller.perform_caching = false 16 | 17 | # Raise exceptions instead of rendering exception templates 18 | config.action_dispatch.show_exceptions = false 19 | 20 | # Disable request forgery protection in test environment 21 | config.action_controller.allow_forgery_protection = false 22 | 23 | # Tell Action Mailer not to deliver emails to the real world. 24 | # The :test delivery method accumulates sent emails in the 25 | # ActionMailer::Base.deliveries array. 26 | config.action_mailer.delivery_method = :test 27 | 28 | # Use SQL instead of Active Record's schema dumper when creating the test database. 29 | # This is necessary if your schema can't be completely dumped by the schema dumper, 30 | # like if you have constraints or database-specific column types 31 | # config.active_record.schema_format = :sql 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | end 36 | -------------------------------------------------------------------------------- /test-project/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test-project/config/initializers/db_charmer.rb: -------------------------------------------------------------------------------- 1 | DbCharmer.connections_should_exist = false # Since we are not in production 2 | DbCharmer.enable_controller_magic! 3 | -------------------------------------------------------------------------------- /test-project/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | DbCharmerSandbox::Application.config.secret_token = 'bf10223f7ec7f4b2f2c6f98545ee0d172d5fe052deeba6e416e34e0a7534bc2f5a9983f331b5a799ac6544bf99d906c2a5a3bee8260d4cb985f2c096527aa3ad' 8 | -------------------------------------------------------------------------------- /test-project/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | DbCharmerSandbox::Application.config.session_store :cookie_store, :key => '_db-charmer-sandbox_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # DbCharmerSandbox::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /test-project/config/initializers/sharding.rb: -------------------------------------------------------------------------------- 1 | # Range-based shards for testing 2 | 3 | TEXTS_SHARDING_RANGES = { 4 | 0...100 => :shard1, 5 | 100..200 => :shard2, 6 | :default => :shard3 7 | } 8 | 9 | DbCharmer::Sharding.register_connection( 10 | :name => :texts, 11 | :method => :range, 12 | :ranges => TEXTS_SHARDING_RANGES 13 | ) 14 | 15 | #------------------------------------------------ 16 | # Db blocks map sharding for testing 17 | 18 | SOCIAL_SHARDING = DbCharmer::Sharding.register_connection( 19 | :name => :social, 20 | :method => :db_block_map, 21 | :block_size => 10, 22 | :map_table => :event_shards_map, 23 | :shards_table => :event_shards_info, 24 | :connection => :social_shard_info 25 | ) 26 | -------------------------------------------------------------------------------- /test-project/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test-project/config/routes.rb: -------------------------------------------------------------------------------- 1 | DbCharmerSandbox::Application.routes.draw do 2 | # The priority is based upon order of creation: 3 | # first created -> highest priority. 4 | 5 | # Resource routes 6 | resources :posts 7 | resources :cars 8 | 9 | # Sample of regular route: 10 | # match 'products/:id' => 'catalog#view' 11 | # Keep in mind you can assign values other than :controller and :action 12 | 13 | # Sample of named route: 14 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 15 | # This route can be invoked with purchase_url(:id => product.id) 16 | 17 | # Sample resource route (maps HTTP verbs to controller actions automatically): 18 | # resources :products 19 | 20 | # Sample resource route with options: 21 | # resources :products do 22 | # member do 23 | # get 'short' 24 | # post 'toggle' 25 | # end 26 | # 27 | # collection do 28 | # get 'sold' 29 | # end 30 | # end 31 | 32 | # Sample resource route with sub-resources: 33 | # resources :products do 34 | # resources :comments, :sales 35 | # resource :seller 36 | # end 37 | 38 | # Sample resource route with more complex sub-resources 39 | # resources :products do 40 | # resources :comments 41 | # resources :sales do 42 | # get 'recent', :on => :collection 43 | # end 44 | # end 45 | 46 | # Sample resource route within a namespace: 47 | # namespace :admin do 48 | # # Directs /admin/products/* to Admin::ProductsController 49 | # # (app/controllers/admin/products_controller.rb) 50 | # resources :products 51 | # end 52 | 53 | # You can have the root of your site routed with "root" 54 | # just remember to delete public/index.html. 55 | # root :to => "welcome#index" 56 | 57 | # See how all your routes lay out with "rake routes" 58 | 59 | # This is a legacy wild controller route that's not recommended for RESTful applications. 60 | # Note: This route will make all actions in every controller accessible via GET requests. 61 | match ':controller(/:action(/:id(.:format)))' 62 | end 63 | -------------------------------------------------------------------------------- /test-project/db/create_databases.sql: -------------------------------------------------------------------------------- 1 | drop database if exists db_charmer_sandbox_test; 2 | create database db_charmer_sandbox_test; 3 | 4 | drop database if exists db_charmer_logs_test; 5 | create database db_charmer_logs_test; 6 | 7 | drop database if exists db_charmer_events_test_shard01; 8 | create database db_charmer_events_test_shard01; 9 | 10 | drop database if exists db_charmer_events_test_shard02; 11 | create database db_charmer_events_test_shard02; 12 | 13 | grant all privileges on db_charmer_sandbox_test.* to 'db_charmer_ro'@'localhost'; 14 | -------------------------------------------------------------------------------- /test-project/db/migrate/20090810013829_create_log_records.rb: -------------------------------------------------------------------------------- 1 | class CreateLogRecords < ActiveRecord::Migration 2 | db_magic :connection => :logs 3 | 4 | def self.up 5 | create_table :log_records do |t| 6 | t.integer :user_id 7 | t.string :level 8 | t.string :message 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :log_records 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test-project/db/migrate/20090810013922_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration 2 | def self.up 3 | create_table :posts do |t| 4 | t.string :title 5 | t.text :body 6 | t.integer :user_id 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :posts 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test-project/db/migrate/20090810221944_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table :users do |t| 4 | t.string :login 5 | t.string :password 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :users 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test-project/db/migrate/20100305234245_create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration 2 | def self.up 3 | create_table :categories do |t| 4 | t.string :name 5 | 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :categories 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test-project/db/migrate/20100305234340_create_categories_posts.rb: -------------------------------------------------------------------------------- 1 | class CreateCategoriesPosts < ActiveRecord::Migration 2 | def self.up 3 | pk_in_join_table = !DbCharmer.rails3? 4 | create_table :categories_posts, :id => pk_in_join_table do |t| 5 | t.integer :post_id 6 | t.integer :category_id 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :categories_posts 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test-project/db/migrate/20100305235831_create_avatars.rb: -------------------------------------------------------------------------------- 1 | class CreateAvatars < ActiveRecord::Migration 2 | def self.up 3 | create_table :avatars do |t| 4 | t.integer :user_id 5 | t.string :name 6 | 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :avatars 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test-project/db/migrate/20100328201317_create_sharding_map_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateShardingMapTables < ActiveRecord::Migration 2 | db_magic :connection => :social_shard_info 3 | 4 | def self.up 5 | create_table :event_shards_info, :force => true do |t| 6 | t.timestamps 7 | t.string :db_host, :null => false 8 | t.integer :db_port, :null => false, :default => 3306 9 | t.string :db_user, :null => false, :default => 'root' 10 | t.string :db_pass, :null => false, :default => '' 11 | t.string :db_name, :null => false 12 | t.boolean :open, :null => false, :default => false 13 | t.boolean :enabled, :null => false, :default => false 14 | t.integer :blocks_count, :null => false, :default => 0 15 | end 16 | 17 | add_index :event_shards_info, [:enabled, :open, :blocks_count], :name => "alloc" 18 | 19 | create_table :event_shards_map, :id => false, :force => true do |t| 20 | t.integer :start_id, :null => false 21 | t.integer :end_id, :null => false 22 | t.integer :shard_id, :null => false 23 | t.integer :block_size, :null => false, :default => 0 24 | t.timestamps 25 | end 26 | 27 | add_index :event_shards_map, [:start_id, :end_id], :unique => true 28 | add_index :event_shards_map, :shard_id 29 | end 30 | 31 | def self.down 32 | drop_table :event_shards_map 33 | drop_table :event_shards_info 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test-project/db/migrate/20100330180517_create_event_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateEventTables < ActiveRecord::Migration 2 | # In test environment just use database.yml-defined connections 3 | if Rails.env.test? 4 | db_magic :connections => [ :social_shard01, :social_shard02 ] 5 | else 6 | db_magic :sharded_connection => :social 7 | end 8 | 9 | def self.up 10 | sql = <<-SQL 11 | CREATE TABLE `timeline_events` ( 12 | `event_id` int(11) NOT NULL AUTO_INCREMENT, 13 | `from_uid` int(11) NOT NULL, 14 | `to_uid` int(11) NOT NULL, 15 | `original_created_at` datetime NOT NULL, 16 | `event_type` int(11) NOT NULL, 17 | `event_data` text, 18 | `replies_count` int(11) NOT NULL DEFAULT '0', 19 | `parent_id` int(11) NOT NULL DEFAULT '0', 20 | `touched_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | `on_profile` int(1) NOT NULL DEFAULT '0', 22 | PRIMARY KEY (`to_uid`,`parent_id`,`touched_at`,`event_id`), 23 | UNIQUE KEY `event_id_and_to_uid_key` (`event_id`,`to_uid`), 24 | KEY `on_profile_index` (`to_uid`,`on_profile`,`touched_at`) 25 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 26 | SQL 27 | execute(sql) 28 | end 29 | 30 | def self.down 31 | drop_table :timeline_events 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test-project/db/migrate/20100817191548_create_cars.rb: -------------------------------------------------------------------------------- 1 | class CreateCars < ActiveRecord::Migration 2 | def self.up 3 | create_table :cars do |t| 4 | t.string :type 5 | t.string :license 6 | t.timestamps 7 | end 8 | end 9 | 10 | def self.down 11 | drop_table :cars 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test-project/db/migrate/20111005193941_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def self.up 3 | create_table :comments do |t| 4 | t.string :commentable_type, :null => false 5 | t.integer :commentable_id, :null => false 6 | t.text :body, :null => false 7 | 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :comments 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test-project/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 => 'Daley', :city => cities.first) 8 | -------------------------------------------------------------------------------- /test-project/db/sharding.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.1.44, for apple-darwin10.2.0 (i386) 2 | -- 3 | -- Host: localhost Database: db_charmer_sandbox_test 4 | -- ------------------------------------------------------ 5 | -- Server version 5.1.44 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES utf8 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `events_shard_info` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `events_shard_info`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!40101 SET character_set_client = utf8 */; 25 | CREATE TABLE `events_shard_info` ( 26 | `id` int(10) unsigned NOT NULL, 27 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 28 | `db_host` varchar(255) NOT NULL, 29 | `db_port` int(10) unsigned NOT NULL DEFAULT '3306', 30 | `db_user` varchar(255) NOT NULL DEFAULT 'root', 31 | `db_pass` varchar(255) NOT NULL DEFAULT '', 32 | `open` tinyint(1) unsigned NOT NULL DEFAULT '0', 33 | `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0', 34 | `blocks_count` int(10) unsigned NOT NULL DEFAULT '0', 35 | PRIMARY KEY (`id`), 36 | KEY `alloc` (`enabled`,`open`,`blocks_count`) 37 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 38 | /*!40101 SET character_set_client = @saved_cs_client */; 39 | 40 | -- 41 | -- Dumping data for table `events_shard_info` 42 | -- 43 | 44 | LOCK TABLES `events_shard_info` WRITE; 45 | /*!40000 ALTER TABLE `events_shard_info` DISABLE KEYS */; 46 | /*!40000 ALTER TABLE `events_shard_info` ENABLE KEYS */; 47 | UNLOCK TABLES; 48 | 49 | -- 50 | -- Table structure for table `events_shard_dict` 51 | -- 52 | 53 | DROP TABLE IF EXISTS `events_shard_dict`; 54 | /*!40101 SET @saved_cs_client = @@character_set_client */; 55 | /*!40101 SET character_set_client = utf8 */; 56 | CREATE TABLE `events_shard_dict` ( 57 | `start_id` int(10) unsigned NOT NULL, 58 | `end_id` int(10) unsigned NOT NULL, 59 | `shard_id` int(10) unsigned NOT NULL, 60 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 61 | `block_size` int(10) unsigned NOT NULL, 62 | PRIMARY KEY (`start_id`,`end_id`), 63 | KEY `shard_id` (`shard_id`) 64 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 65 | /*!40101 SET character_set_client = @saved_cs_client */; 66 | 67 | -- 68 | -- Dumping data for table `events_shard_dict` 69 | -- 70 | 71 | LOCK TABLES `events_shard_dict` WRITE; 72 | /*!40000 ALTER TABLE `events_shard_dict` DISABLE KEYS */; 73 | /*!40000 ALTER TABLE `events_shard_dict` ENABLE KEYS */; 74 | UNLOCK TABLES; 75 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 76 | 77 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 78 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 79 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 80 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 81 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 82 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 83 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 84 | 85 | -- Dump completed on 2010-03-22 1:37:30 86 | -------------------------------------------------------------------------------- /test-project/spec/controllers/posts_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PostsController do 4 | fixtures :posts 5 | 6 | # Delete these examples and add some real ones 7 | it "should support db_charmer readonly actions method" do 8 | PostsController.respond_to?(:force_slave_reads).should be_true 9 | end 10 | 11 | it "index action should force slave reads" do 12 | PostsController.force_slave_reads_action?(:index).should be_true 13 | end 14 | 15 | it "create action should not force slave reads" do 16 | PostsController.force_slave_reads_action?(:create).should be_false 17 | end 18 | 19 | describe "GET 'index'" do 20 | context "slave reads enforcing (action is listed in :only)" do 21 | it "should enable enforcing" do 22 | get 'index' 23 | controller.force_slave_reads?.should be_true 24 | end 25 | 26 | it "should actually force slave reads" do 27 | Post.connection.should_not_receive(:select_value) # no counts 28 | Post.connection.should_not_receive(:select_all) # no finds 29 | Post.on_slave.connection.should_receive(:select_value).and_return(1) 30 | get 'index' 31 | end 32 | end 33 | end 34 | 35 | describe "GET 'show'" do 36 | context "slave reads enforcing (action is listed in :only)" do 37 | it "should enable enforcing" do 38 | get 'show', :id => Post.first.id 39 | controller.force_slave_reads?.should be_true 40 | end 41 | 42 | it "should actually force slave reads" do 43 | post = Post.first 44 | Post.connection.should_not_receive(:select_value) # no counts 45 | Post.connection.should_not_receive(:select_all) # no finds 46 | Post.on_slave.connection.should_receive(:select_value).and_return(1) 47 | Post.on_slave.connection.should_receive(:select_all).and_return([post.attributes]) 48 | get 'show', :id => post.id 49 | end 50 | end 51 | end 52 | 53 | describe "GET 'new'" do 54 | context "slave reads enforcing (action is listed in :except)" do 55 | it "should not enable enforcing" do 56 | get 'new' 57 | controller.force_slave_reads?.should be_false 58 | end 59 | 60 | it "should not do any actual enforcing" do 61 | Post.connection.should_receive(:select_value).and_return(0) # count 62 | Post.on_slave.connection.should_not_receive(:select_value) # no counts 63 | Post.on_slave.connection.should_not_receive(:select_all) # no selects 64 | get 'new' 65 | end 66 | end 67 | end 68 | 69 | describe "GET 'create'" do 70 | it "should redirect to post url upon successful completion" do 71 | get 'create', :post => { :title => 'xxx', :user_id => 1 } 72 | response.should redirect_to(post_url(Post.last)) 73 | end 74 | 75 | it "should create a Post record" do 76 | lambda { 77 | get 'create', :post => { :title => 'xxx', :user_id => 1 } 78 | }.should change { Post.count }.by(+1) 79 | end 80 | 81 | context "slave reads enforcing (action is not listed in force_slave_reads params)" do 82 | it "should not enable enforcing" do 83 | get 'create' 84 | controller.force_slave_reads?.should_not be_true 85 | end 86 | 87 | it "should not do any actual enforcing" do 88 | Post.on_slave.connection.should_not_receive(:select_value) 89 | Post.connection.should_receive(:select_value).once.and_return(1) 90 | get 'create' 91 | end 92 | end 93 | end 94 | 95 | describe "GET 'destroy'" do 96 | it "should redurect to index upon completion" do 97 | get 'destroy', :id => Post.first.id 98 | response.should redirect_to(:action => :index) 99 | end 100 | 101 | it "should delete a record" do 102 | lambda { 103 | get 'destroy', :id => Post.first.id 104 | }.should change { Post.count }.by(-1) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/avatars.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | user_id: 1 5 | name: avatar1 6 | 7 | two: 8 | user_id: 2 9 | name: avatar2 10 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/categories.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | id: 1 5 | name: one 6 | 7 | two: 8 | id: 2 9 | name: two 10 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/categories_posts.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one_one: 4 | post_id: 1 5 | category_id: 1 6 | 7 | one_two: 8 | post_id: 1 9 | category_id: 2 10 | 11 | two_one: 12 | post_id: 2 13 | category_id: 1 14 | 15 | windoze_two: 16 | post_id: 4 17 | category_id: 2 18 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | avatar: 2 | commentable: one (Avatar) 3 | body: "This is an avatar" 4 | 5 | post: 6 | commentable: one (Post) 7 | body: "This is a post" 8 | 9 | user: 10 | commentable: one (User) 11 | body: "This is a user" 12 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/event_shards_info.yml: -------------------------------------------------------------------------------- 1 | shard1: 2 | id: 1 3 | db_host: localhost 4 | db_name: db_charmer_events_test_shard01 5 | open: 1 6 | enabled: 1 7 | blocks_count: 2 8 | created_at: <%= Time.now.to_s(:db) %> 9 | updated_at: <%= Time.now.to_s(:db) %> 10 | 11 | shard2: 12 | id: 2 13 | db_host: localhost 14 | db_name: db_charmer_events_test_shard02 15 | open: 1 16 | enabled: 1 17 | blocks_count: 1 18 | created_at: <%= Time.now.to_s(:db) %> 19 | updated_at: <%= Time.now.to_s(:db) %> 20 | 21 | empty: 22 | id: 3 23 | db_host: localhost 24 | db_name: db_charmer_events_test_shard01 25 | open: 1 26 | enabled: 1 27 | blocks_count: 0 28 | created_at: <%= Time.now.to_s(:db) %> 29 | updated_at: <%= Time.now.to_s(:db) %> 30 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/event_shards_map.yml: -------------------------------------------------------------------------------- 1 | block1: 2 | start_id: 0 3 | end_id: 10 4 | shard_id: 1 5 | block_size: 10 6 | created_at: <%= Time.now.to_s(:db) %> 7 | updated_at: <%= Time.now.to_s(:db) %> 8 | 9 | block2: 10 | start_id: 10 11 | end_id: 20 12 | shard_id: 2 13 | block_size: 10 14 | created_at: <%= Time.now.to_s(:db) %> 15 | updated_at: <%= Time.now.to_s(:db) %> 16 | 17 | block3: 18 | start_id: 20 19 | end_id: 30 20 | shard_id: 1 21 | block_size: 10 22 | created_at: <%= Time.now.to_s(:db) %> 23 | updated_at: <%= Time.now.to_s(:db) %> 24 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/log_records.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | level: MyString 5 | message: MyString 6 | 7 | two: 8 | level: MyString 9 | message: MyString 10 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/posts.yml: -------------------------------------------------------------------------------- 1 | one: 2 | id: 1 3 | title: MyString 4 | body: MyText 5 | user_id: 1 6 | 7 | two: 8 | id: 2 9 | title: MyString 10 | body: MyText 11 | user_id: 2 12 | 13 | windoze: 14 | id: 3 15 | title: Windows Sucks 16 | body: Yeah, it does! 17 | user_id: 3 18 | 19 | foo: 20 | id: 4 21 | title: Foo 22 | body: Foo body 23 | user_id: 3 24 | -------------------------------------------------------------------------------- /test-project/spec/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | id: 1 5 | login: MyString 6 | password: MyString 7 | 8 | two: 9 | id: 2 10 | login: MyString 11 | password: MyString 12 | 13 | bill: 14 | id: 3 15 | login: bill 16 | password: windoze 17 | -------------------------------------------------------------------------------- /test-project/spec/integration/multi_threading_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "DbCharmer integration tests" do 4 | def do_test(test_seconds, thread_count) 5 | start_time = Time.now.to_f 6 | threads = Array.new 7 | 8 | while threads.size < thread_count 9 | threads << Thread.new do 10 | while Time.now.to_f - start_time < test_seconds do 11 | User.create!(:login => "user#{rand}", :password => rand) 12 | User.uncached { User.on_db(:slave01).first } 13 | end 14 | end 15 | end 16 | 17 | # Wait for threads to finish 18 | threads.each(&:join) 19 | end 20 | 21 | it "should work in single-threaded mode" do 22 | do_test(10, 1) 23 | end 24 | 25 | it "should work with 5 threads" do 26 | do_test(10, 5) 27 | end 28 | 29 | it "should use default connection passed in db_magic call in all threads" do 30 | # Define a class with db magic in it 31 | class TestLogRecordWithThreads < ActiveRecord::Base 32 | self.table_name = :log_records 33 | db_magic :connection => :logs 34 | end 35 | 36 | # Check conection in the same thread 37 | TestLogRecordWithThreads.connection.db_charmer_connection_name.should == "logs" 38 | 39 | # Check connection in a different thread 40 | Thread.new { 41 | TestLogRecordWithThreads.connection.db_charmer_connection_name.should == "logs" 42 | }.join 43 | end 44 | 45 | it "should use default connection passed in db_magic call when master connection is being remapped" do 46 | class TestLogRecordWithThreadsAndRemapping < ActiveRecord::Base 47 | self.table_name = :log_records 48 | db_magic :connection => :logs 49 | end 50 | 51 | # Test in main thread 52 | expect { 53 | DbCharmer.with_remapped_databases(:master => :slave01) do 54 | TestLogRecordWithThreadsAndRemapping.first 55 | end 56 | }.to_not raise_error 57 | 58 | # Test in another thread 59 | Thread.new { 60 | expect { 61 | DbCharmer.with_remapped_databases(:master => :slave01) do 62 | TestLogRecordWithThreadsAndRemapping.first 63 | end 64 | }.to_not raise_error 65 | }.join 66 | end 67 | end unless ENV['SKIP_MT_TESTS'] 68 | -------------------------------------------------------------------------------- /test-project/spec/models/avatar_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Avatar do 4 | before(:each) do 5 | @valid_attributes = { 6 | :user_id => 1, 7 | :name => "value for name" 8 | } 9 | end 10 | 11 | it "should create a new instance given valid attributes" do 12 | Avatar.create!(@valid_attributes) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test-project/spec/models/cars_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ford, "STI model" do 4 | before(:each) do 5 | @valid_attributes = { 6 | :license => "FFGH-9134" 7 | } 8 | end 9 | 10 | it "should create a new instance given valid attributes" do 11 | Ford.create!(@valid_attributes) 12 | end 13 | 14 | it "should properly handle slave find calls" do 15 | Ford.first.should be_valid 16 | end 17 | end 18 | 19 | describe Toyota, "STI model" do 20 | before(:each) do 21 | @valid_attributes = { 22 | :license => "TFGH-9134" 23 | } 24 | end 25 | 26 | it "should create a new instance given valid attributes" do 27 | Toyota.create!(@valid_attributes) 28 | end 29 | 30 | it "should properly handle slave find calls" do 31 | Toyota.first.should be_valid 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test-project/spec/models/categories_posts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe CategoriesPosts do 4 | before(:each) do 5 | @valid_attributes = { 6 | :post_id => 1, 7 | :category_id => 1 8 | } 9 | end 10 | 11 | it "should create a new instance given valid attributes" do 12 | CategoriesPosts.create!(@valid_attributes) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test-project/spec/models/category_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Category do 4 | before(:each) do 5 | @valid_attributes = { 6 | :name => "value for name" 7 | } 8 | end 9 | 10 | it "should create a new instance given valid attributes" do 11 | Category.create!(@valid_attributes) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test-project/spec/models/comment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Comment do 4 | fixtures :comments, :avatars, :posts, :users 5 | 6 | describe "preload polymorphic association" do 7 | subject do 8 | lambda { 9 | Comment.find(:all, :include => :commentable) 10 | } 11 | end 12 | 13 | it { should_not raise_error } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test-project/spec/models/event_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Event, "sharded model" do 4 | fixtures :event_shards_info, :event_shards_map 5 | 6 | it "should respond to shard_for method" do 7 | Event.should respond_to(:shard_for) 8 | end 9 | 10 | it "should correctly switch shards" do 11 | # Cleanup sharded tables 12 | Event.on_each_shard { |event| event.delete_all } 13 | 14 | # Check that they are empty 15 | Event.shard_for(2).all.should be_empty 16 | Event.shard_for(12).all.should be_empty 17 | 18 | # Create some data (one record in each shard) 19 | Event.shard_for(2).create!( 20 | :from_uid => 1, 21 | :to_uid => 2, 22 | :original_created_at => Time.now, 23 | :event_type => 1, 24 | :event_data => 'foo' 25 | ) 26 | Event.shard_for(12).create!( 27 | :from_uid => 1, 28 | :to_uid => 12, 29 | :original_created_at => Time.now, 30 | :event_type => 1, 31 | :event_data => 'bar' 32 | ) 33 | 34 | # Check sharded tables to make sure they have the data 35 | Event.shard_for(2).find_all_by_from_uid(1).map(&:event_data).should == [ 'foo' ] 36 | Event.shard_for(12).find_all_by_from_uid(1).map(&:event_data).should == [ 'bar' ] 37 | end 38 | 39 | it "should allocate new blocks when needed" do 40 | # Cleanup sharded tables 41 | Event.on_each_shard { |event| event.delete_all } 42 | 43 | # Check new block, it should be empty 44 | Event.shard_for(100).count.should be_zero 45 | 46 | # Create an object 47 | Event.shard_for(100).create!( 48 | :from_uid => 1, 49 | :to_uid => 100, 50 | :original_created_at => Time.now, 51 | :event_type => 1, 52 | :event_data => 'blah' 53 | ) 54 | 55 | # Check the new block 56 | Event.shard_for(100).count.should == 1 57 | end 58 | 59 | it "should fail to perform any database operations w/o a shard specification" do 60 | Event.stub(:column_defaults).and_return({}) 61 | Event.stub(:columns_hash).and_return({}) 62 | 63 | lambda { Event.first }.should raise_error(ActiveRecord::ConnectionNotEstablished) 64 | lambda { Event.create }.should raise_error(ActiveRecord::ConnectionNotEstablished) 65 | lambda { Event.delete_all }.should raise_error(ActiveRecord::ConnectionNotEstablished) 66 | end 67 | 68 | it "should not fail when AR does some internal calls to the database" do 69 | # Cleanup sharded tables 70 | Event.on_each_shard { |event| event.delete_all } 71 | 72 | # Create an object 73 | x = Event.shard_for(100).create!( 74 | :from_uid => 1, 75 | :to_uid => 100, 76 | :original_created_at => Time.now, 77 | :event_type => 1, 78 | :event_data => 'blah' 79 | ) 80 | 81 | Event.reset_column_information 82 | lambda { x.inspect }.should_not raise_error 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test-project/spec/models/log_record_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LogRecord do 4 | before(:each) do 5 | @valid_attributes = { 6 | :level => "value for level", 7 | :message => "value for message" 8 | } 9 | end 10 | 11 | it "should create a new instance given valid attributes" do 12 | LogRecord.create!(@valid_attributes) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test-project/spec/models/post_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Post do 4 | before(:each) do 5 | @valid_attributes = { 6 | :title => "value for title", 7 | :body => "value for body" 8 | } 9 | end 10 | 11 | it "should create a new instance given valid attributes" do 12 | Post.create!(@valid_attributes) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test-project/spec/models/range_sharded_model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RangeShardedModel do 4 | describe "class method shard_for" do 5 | describe "should correctly set shards in range-defined shards" do 6 | [ 0, 1, 50, 99].each do |id| 7 | it "for #{id}" do 8 | RangeShardedModel.shard_for(id) do |m| 9 | m.connection.object_id.should == RangeShardedModel.on_db(:shard1).connection.object_id 10 | end 11 | end 12 | end 13 | 14 | [ 100, 101, 150, 199, 200].each do |id| 15 | it "for #{id}" do 16 | RangeShardedModel.shard_for(id) do |m| 17 | m.connection.object_id.should == RangeShardedModel.on_db(:shard2).connection.object_id 18 | end 19 | end 20 | end 21 | end 22 | 23 | describe "should correctly set shards in default shard" do 24 | [ 201, 500].each do |id| 25 | it "for #{id}" do 26 | RangeShardedModel.shard_for(id) do |m| 27 | m.connection.object_id.should == RangeShardedModel.on_db(:shard3).connection.object_id 28 | end 29 | end 30 | end 31 | end 32 | 33 | it "should raise an exception when there is no default shard and no ranged shards matched" do 34 | begin 35 | default_shard = RangeShardedModel.sharded_connection.sharder.ranges.delete(:default) 36 | lambda { RangeShardedModel.shard_for(500) }.should raise_error(ArgumentError) 37 | ensure 38 | RangeShardedModel.sharded_connection.sharder.ranges[:default] = default_shard 39 | end 40 | end 41 | end 42 | end 43 | 44 | -------------------------------------------------------------------------------- /test-project/spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | before(:each) do 5 | @valid_attributes = { 6 | :login => "value for login", 7 | :password => "value for password" 8 | } 9 | User.switch_connection_to(nil) 10 | User.db_charmer_default_connection = nil 11 | end 12 | 13 | it "should create a new instance given valid attributes" do 14 | User.create!(@valid_attributes) 15 | end 16 | 17 | it "should create a new instance in a specified db" do 18 | # Just to make sure 19 | User.on_db(:user_master).connection.object_id.should_not == User.connection.object_id 20 | 21 | # Default connection should not be touched 22 | User.connection.should_not_receive(:insert) 23 | 24 | # Only specified connection receives an insert 25 | User.on_db(:user_master).connection.should_receive(:insert) 26 | 27 | # Test! 28 | User.on_db(:user_master).create!(@valid_attributes) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test-project/spec/sharding/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer::Sharding::Connection do 4 | describe "in constructor" do 5 | it "should not fail if method name is correct" do 6 | lambda { DbCharmer::Sharding::Connection.new(:name => :foo, :method => :range, :ranges => {}) }.should_not raise_error 7 | end 8 | 9 | it "should fail if method name is missing" do 10 | lambda { DbCharmer::Sharding::Connection.new(:name => :foo) }.should raise_error(ArgumentError) 11 | end 12 | 13 | it "should fail if method name is invalid" do 14 | lambda { DbCharmer::Sharding::Connection.new(:name => :foo, :method => :foo) }.should raise_error(NameError) 15 | end 16 | 17 | it "should instantiate a sharder class according to the :method value" do 18 | DbCharmer::Sharding::Method::Range.should_receive(:new) 19 | DbCharmer::Sharding::Connection.new(:name => :foo, :method => :range, :ranges => {}) 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /test-project/spec/sharding/method/db_block_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer::Sharding::Method::DbBlockMap do 4 | fixtures :event_shards_info, :event_shards_map 5 | 6 | before(:each) do 7 | @sharder = DbCharmer::Sharding::Method::DbBlockMap.new( 8 | :name => :social, 9 | :block_size => 10, 10 | :map_table => :event_shards_map, 11 | :shards_table => :event_shards_info, 12 | :connection => :social_shard_info 13 | ) 14 | @conn = DbCharmer::ConnectionFactory.connect(:social_shard_info) 15 | end 16 | 17 | describe "standard interface" do 18 | it "should respond to shard_for_id" do 19 | @sharder.should respond_to(:shard_for_key) 20 | end 21 | 22 | it "should return a shard config to be used for a key" do 23 | @sharder.shard_for_key(1).should be_kind_of(Hash) 24 | end 25 | 26 | it "should have shard_connections method and return a list of db connections" do 27 | @sharder.shard_connections.should_not be_empty 28 | end 29 | end 30 | 31 | it "should correctly return shards for all blocks defined in the mapping table" do 32 | blocks = @conn.select_all("SELECT * FROM event_shards_map") 33 | 34 | blocks.each do |blk| 35 | shard = @sharder.shard_for_key(blk['start_id']) 36 | shard[:connection_name].should match(/social.*#{blk['shard_id']}$/) 37 | 38 | shard = @sharder.shard_for_key(blk['start_id'].to_i + 1) 39 | shard[:connection_name].should match(/social.*#{blk['shard_id']}$/) 40 | 41 | shard = @sharder.shard_for_key(blk['end_id'].to_i - 1) 42 | shard[:connection_name].should match(/social.*#{blk['shard_id']}$/) 43 | end 44 | end 45 | 46 | describe "for non-existing blocks" do 47 | before do 48 | @max_id = @conn.select_value("SELECT max(end_id) FROM event_shards_map").to_i 49 | Rails.cache.clear 50 | end 51 | 52 | it "should not fail" do 53 | lambda { 54 | @sharder.shard_for_key(@max_id + 1) 55 | }.should_not raise_error 56 | end 57 | 58 | it "should create a new one" do 59 | @sharder.shard_for_key(@max_id + 1).should_not be_nil 60 | end 61 | 62 | it "should assign it to the least loaded shard" do 63 | @sharder.shard_for_key(@max_id + 1)[:connection_name].should match(/shard.*03$/) 64 | end 65 | 66 | it "should not consider non-open shards" do 67 | @conn.execute("UPDATE event_shards_info SET open = 0 WHERE id = 3") 68 | @sharder.shard_for_key(@max_id + 1)[:connection_name].should_not match(/shard.*03$/) 69 | end 70 | 71 | it "should not consider disabled shards" do 72 | @conn.execute("UPDATE event_shards_info SET enabled = 0 WHERE id = 3") 73 | @sharder.shard_for_key(@max_id + 1)[:connection_name].should_not match(/shard.*03$/) 74 | end 75 | 76 | it "should increment the blocks counter on the shard" do 77 | lambda { 78 | @sharder.shard_for_key(@max_id + 1) 79 | }.should change { 80 | @conn.select_value("SELECT blocks_count FROM event_shards_info WHERE id = 3").to_i 81 | }.by(+1) 82 | end 83 | 84 | it "should raise duplicate key error when allocating same block twice" do 85 | @sharder.allocate_new_block_for_key(@max_id + 1) 86 | lambda { 87 | @sharder.allocate_new_block_for_key(@max_id + 1) 88 | }.should raise_error(ActiveRecord::StatementInvalid) 89 | end 90 | 91 | it "should handle duplicate key errors" do 92 | @sharder.shard_for_key(@max_id + 1) 93 | 94 | actual_block = @sharder.block_for_key(@max_id + 1) 95 | @sharder.should_receive(:block_for_key).twice.and_return(nil, actual_block) 96 | 97 | @sharder.shard_for_key(@max_id + 1) 98 | end 99 | end 100 | 101 | it "should fail on invalid shard references" do 102 | @conn.execute("DELETE FROM event_shards_info") 103 | lambda { @sharder.shard_for_key(1) }.should raise_error(ArgumentError) 104 | end 105 | 106 | it "should cache shards info" do 107 | shard = DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.first 108 | DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.should_receive(:find_by_id).once.and_return(shard) 109 | @sharder.shard_info_by_id(1) 110 | @sharder.shard_info_by_id(1) 111 | end 112 | 113 | it "should not cache shards info when explicitly asked not to" do 114 | shard = DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.first 115 | DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.should_receive(:find_by_id).twice.and_return(shard) 116 | @sharder.shard_info_by_id(1, false) 117 | @sharder.shard_info_by_id(1, false) 118 | end 119 | 120 | it "should cache blocks" do 121 | @sharder.block_for_key(1) 122 | @sharder.connection.should_not_receive(:select_one) 123 | @sharder.block_for_key(1) 124 | @sharder.block_for_key(2) 125 | end 126 | 127 | it "should not cache blocks if asked not to" do 128 | block = @sharder.block_for_key(1) 129 | @sharder.connection.should_receive(:select_one).twice.and_return(block) 130 | @sharder.block_for_key(1, false) 131 | @sharder.block_for_key(2, false) 132 | end 133 | 134 | 135 | end 136 | -------------------------------------------------------------------------------- /test-project/spec/sharding/method/hash_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer::Sharding::Method::HashMap do 4 | SHARDING_MAP = { 5 | 'US' => :us_users, 6 | 'CA' => :ca_users, 7 | :default => :other_users 8 | } 9 | 10 | before do 11 | @sharder = DbCharmer::Sharding::Method::HashMap.new(:map => SHARDING_MAP) 12 | end 13 | 14 | describe "standard interface" do 15 | it "should respond to shard_for_id" do 16 | @sharder.should respond_to(:shard_for_key) 17 | end 18 | 19 | it "should return a shard name to be used for an key" do 20 | @sharder.shard_for_key('US').should be_kind_of(Symbol) 21 | end 22 | 23 | it "should support default shard" do 24 | @sharder.support_default_shard?.should be_true 25 | end 26 | end 27 | 28 | describe "should correctly return shards for all keys defined in the map" do 29 | SHARDING_MAP.except(:default).each do |key, val| 30 | it "for #{key}" do 31 | @sharder.shard_for_key(key).should == val 32 | end 33 | end 34 | end 35 | 36 | it "should correctly return default shard" do 37 | @sharder.shard_for_key('UA').should == :other_users 38 | end 39 | 40 | it "should raise an exception when there is no default shard and nothing matched" do 41 | @sharder.map.delete(:default) 42 | lambda { @sharder.shard_for_key('UA') }.should raise_error(ArgumentError) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test-project/spec/sharding/method/range_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer::Sharding::Method::Range do 4 | SHARDING_RANGES = { 5 | 0...100 => :shard1, 6 | 100..200 => :shard2, 7 | :default => :shard3 8 | } 9 | 10 | before do 11 | @sharder = DbCharmer::Sharding::Method::Range.new(:ranges => SHARDING_RANGES) 12 | end 13 | 14 | describe "standard interface" do 15 | it "should respond to shard_for_id" do 16 | @sharder.should respond_to(:shard_for_key) 17 | end 18 | 19 | it "should return a shard name to be used for an key" do 20 | @sharder.shard_for_key(1).should be_kind_of(Symbol) 21 | end 22 | 23 | it "should support default shard" do 24 | @sharder.support_default_shard?.should be_true 25 | end 26 | end 27 | 28 | describe "should correctly return shards for all ids in defined ranges" do 29 | [ 0, 1, 50, 99].each do |id| 30 | it "for #{id}" do 31 | @sharder.shard_for_key(id).should == :shard1 32 | end 33 | end 34 | 35 | [ 100, 101, 150, 199, 200].each do |id| 36 | it "for #{id}" do 37 | @sharder.shard_for_key(id).should == :shard2 38 | end 39 | end 40 | end 41 | 42 | describe "should correctly return shard for all ids outside the ranges if has a default" do 43 | [ 201, 500].each do |id| 44 | it "for #{id}" do 45 | @sharder.shard_for_key(id).should == :shard3 46 | end 47 | end 48 | end 49 | 50 | it "should raise an exception when there is no default shard and no ranges matched" do 51 | @sharder.ranges.delete(:default) 52 | lambda { @sharder.shard_for_key(500) }.should raise_error(ArgumentError) 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test-project/spec/sharding/sharding_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "DbCharmer::Sharding" do 4 | describe "in register_connection method" do 5 | it "should raise an exception if passed config has no :name parameter" do 6 | lambda { 7 | DbCharmer::Sharding.register_connection(:method => :range, :ranges => { :default => :foo }) 8 | }.should raise_error(ArgumentError) 9 | end 10 | 11 | it "should not raise an exception if passed config has all required params" do 12 | lambda { 13 | DbCharmer::Sharding.register_connection(:method => :range, :ranges => { :default => :foo }, :name => :foo) 14 | }.should_not raise_error 15 | end 16 | end 17 | 18 | describe "in sharded_connection method" do 19 | it "should raise an error for invalid connection names" do 20 | lambda { DbCharmer::Sharding.sharded_connection(:blah) }.should raise_error(ArgumentError) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test-project/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] = 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | 6 | # Requires supporting ruby files with custom matchers and macros, etc, 7 | # in spec/support/ and its subdirectories. 8 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 9 | 10 | RSpec.configure do |config| 11 | # == Mock Framework 12 | # 13 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 14 | # 15 | # config.mock_with :mocha 16 | # config.mock_with :flexmock 17 | # config.mock_with :rr 18 | config.mock_with :rspec 19 | 20 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 21 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 22 | 23 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 24 | # examples within a transaction, remove the following line or assign false 25 | # instead of true. 26 | config.use_transactional_fixtures = false 27 | config.use_instantiated_fixtures = false 28 | end 29 | -------------------------------------------------------------------------------- /test-project/spec/support/rails31_stub_connection.rb: -------------------------------------------------------------------------------- 1 | def stub_columns_for_rails31(connection) 2 | return unless DbCharmer.rails31? 3 | connection.abstract_connection_class.retrieve_connection.stub(:columns).and_return([]) 4 | end 5 | -------------------------------------------------------------------------------- /test-project/spec/unit/abstract_adapter/log_formatting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if DbCharmer.rails2? 4 | describe 'AbstractAdapter' do 5 | it "should respond to connection_name accessor" do 6 | ActiveRecord::Base.connection.respond_to?(:connection_name).should be_true 7 | end 8 | 9 | it "should have connection_name read accessor working" do 10 | DbCharmer::ConnectionFactory.generate_abstract_class('logs').connection.connection_name.should == 'logs' 11 | DbCharmer::ConnectionFactory.generate_abstract_class('slave01').connection.connection_name.should == 'slave01' 12 | ActiveRecord::Base.connection.connection_name.should be_nil 13 | end 14 | 15 | it "should append connection name to log records on non-default connections" do 16 | User.switch_connection_to nil 17 | default_message = User.connection.send(:format_log_entry, 'hello world') 18 | switched_message = User.on_db(:slave01).connection.send(:format_log_entry, 'hello world') 19 | switched_message.should_not == default_message 20 | switched_message.should match(/slave01/) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test-project/spec/unit/action_controller/force_slave_reads_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class BlahController < ActionController::Base; end 4 | 5 | describe ActionController, "with force_slave_reads extension" do 6 | before do 7 | BlahController.force_slave_reads({}) # cleanup status 8 | end 9 | 10 | it "should not force slave reads when there are no actions defined as forced" do 11 | BlahController.force_slave_reads_action?(:index).should be_false 12 | end 13 | 14 | it "should force slave reads for :only actions" do 15 | BlahController.force_slave_reads :only => :index 16 | BlahController.force_slave_reads_action?(:index).should be_true 17 | end 18 | 19 | it "should not force slave reads for non-listed actions when there is :only parameter" do 20 | BlahController.force_slave_reads :only => :index 21 | BlahController.force_slave_reads_action?(:show).should be_false 22 | end 23 | 24 | it "should not force slave reads for :except actions" do 25 | BlahController.force_slave_reads :except => :delete 26 | BlahController.force_slave_reads_action?(:delete).should be_false 27 | end 28 | 29 | it "should force slave reads for non-listed actions when there is :except parameter" do 30 | BlahController.force_slave_reads :except => :delete 31 | BlahController.force_slave_reads_action?(:index).should be_true 32 | end 33 | 34 | it "should not force slave reads for actions listed in both :except and :only lists" do 35 | BlahController.force_slave_reads :only => :delete, :except => :delete 36 | BlahController.force_slave_reads_action?(:delete).should be_false 37 | end 38 | 39 | it "should not force slave reads for non-listed actions when there are :except and :only lists present" do 40 | BlahController.force_slave_reads :only => :index, :except => :delete 41 | BlahController.force_slave_reads_action?(:show).should be_false 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/association_preload_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if DbCharmer.rails2? 4 | describe "ActiveRecord preload_associations method" do 5 | it "should be public" do 6 | ActiveRecord::Base.public_methods.collect(&:to_s).member?('preload_associations').should be_true 7 | end 8 | end 9 | end 10 | 11 | describe "ActiveRecord in finder methods" do 12 | fixtures :categories, :users, :posts, :categories_posts, :avatars 13 | 14 | before do 15 | Post.db_magic :connection => nil 16 | User.db_magic :connection => nil 17 | end 18 | 19 | after do 20 | Post.db_magic(Post::DB_MAGIC_DEFAULT_PARAMS) 21 | end 22 | 23 | it "should switch all belongs_to association connections when :include is used" do 24 | User.connection.should_not_receive(:select_all) 25 | Post.on_db(:slave01).all(:include => :user) 26 | end 27 | 28 | it "should switch all has_many association connections when :include is used" do 29 | Post.connection.should_not_receive(:select_all) 30 | User.on_db(:slave01).all(:include => :posts) 31 | end 32 | 33 | it "should switch all has_one association connections when :include is used" do 34 | Avatar.connection.should_not_receive(:select_all) 35 | User.on_db(:slave01).all(:include => :avatar) 36 | end 37 | 38 | it "should switch all has_and_belongs_to_many association connections when :include is used" do 39 | Post.connection.should_not_receive(:select_all) 40 | Category.on_db(:slave01).all(:include => :posts) 41 | end 42 | 43 | #------------------------------------------------------------------------------------------- 44 | it "should not switch assocations when called on a top-level connection" do 45 | User.connection.should_receive(:select_all).and_return([]) 46 | Post.all(:include => :user) 47 | end 48 | 49 | it "should not switch connection when association model and main model are on different servers" do 50 | LogRecord.connection.should_receive(:select_all).and_return([]) 51 | User.on_db(:slave01).all(:include => :log_records) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/association_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "DbCharmer::AssociationProxy extending AR::Associations" do 4 | fixtures :users, :posts 5 | 6 | it "should add proxy? => true method" do 7 | users(:bill).posts.proxy?.should be_true 8 | end 9 | 10 | describe "in has_many associations" do 11 | before do 12 | @user = users(:bill) 13 | @posts = @user.posts.all 14 | Post.switch_connection_to(:logs) 15 | User.switch_connection_to(:logs) 16 | end 17 | 18 | after do 19 | Post.switch_connection_to(nil) 20 | User.switch_connection_to(nil) 21 | end 22 | 23 | it "should implement on_db proxy" do 24 | Post.connection.should_not_receive(:select_all) 25 | User.connection.should_not_receive(:select_all) 26 | 27 | stub_columns_for_rails31 Post.on_db(:logs).connection 28 | Post.on_db(:slave01).connection.should_receive(:select_all).and_return(@posts.map { |p| p.attributes }) 29 | assert_equal @posts, @user.posts.on_db(:slave01) 30 | end 31 | 32 | it "on_db should work in prefix mode" do 33 | Post.connection.should_not_receive(:select_all) 34 | User.connection.should_not_receive(:select_all) 35 | 36 | stub_columns_for_rails31 Post.on_db(:logs).connection 37 | Post.on_db(:slave01).connection.should_receive(:select_all).and_return(@posts.map { |p| p.attributes }) 38 | @user.on_db(:slave01).posts.should == @posts 39 | end 40 | 41 | it "should actually proxy calls to the rails association proxy" do 42 | Post.switch_connection_to(nil) 43 | @user.posts.on_db(:slave01).count.should == @user.posts.count 44 | end 45 | 46 | it "should work with named scopes" do 47 | Post.switch_connection_to(nil) 48 | @user.posts.windows_posts.on_db(:slave01).count.should == @user.posts.windows_posts.count 49 | end 50 | 51 | it "should work with chained named scopes" do 52 | Post.switch_connection_to(nil) 53 | @user.posts.windows_posts.dummy_scope.on_db(:slave01).count.should == @user.posts.windows_posts.dummy_scope.count 54 | end 55 | end 56 | 57 | describe "in belongs_to associations" do 58 | before do 59 | @post = posts(:windoze) 60 | @user = users(:bill) 61 | User.switch_connection_to(:logs) 62 | User.connection.object_id.should_not == Post.connection.object_id 63 | end 64 | 65 | after do 66 | User.switch_connection_to(nil) 67 | end 68 | 69 | it "should implement on_db proxy" do 70 | pending 71 | Post.connection.should_not_receive(:select_all) 72 | User.connection.should_not_receive(:select_all) 73 | User.on_db(:slave01).connection.should_receive(:select_all).once.and_return([ @user ]) 74 | @post.user.on_db(:slave01).should == @post.user 75 | end 76 | 77 | it "on_db should work in prefix mode" do 78 | pending 79 | Post.connection.should_not_receive(:select_all) 80 | User.connection.should_not_receive(:select_all) 81 | User.on_db(:slave01).connection.should_receive(:select_all).once.and_return([ @user ]) 82 | @post.on_db(:slave01).user.should == @post.user 83 | end 84 | 85 | it "should actually proxy calls to the rails association proxy" do 86 | User.switch_connection_to(nil) 87 | @post.user.on_db(:slave01).should == @post.user 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/class_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class FooModel < ActiveRecord::Base; end 4 | 5 | describe DbCharmer, "for ActiveRecord models" do 6 | context "in db_charmer_connection_proxy methods" do 7 | before do 8 | FooModel.db_charmer_connection_proxy = nil 9 | FooModel.db_charmer_default_connection = nil 10 | end 11 | 12 | it "should implement both accessor methods" do 13 | proxy = double('connection proxy') 14 | FooModel.db_charmer_connection_proxy = proxy 15 | FooModel.db_charmer_connection_proxy.should be(proxy) 16 | end 17 | end 18 | 19 | context "in db_charmer_default_connection methods" do 20 | before do 21 | FooModel.db_charmer_default_connection = nil 22 | FooModel.db_charmer_default_connection = nil 23 | end 24 | 25 | it "should implement both accessor methods" do 26 | conn = double('connection') 27 | FooModel.db_charmer_default_connection = conn 28 | FooModel.db_charmer_default_connection.should be(conn) 29 | end 30 | end 31 | 32 | context "in db_charmer_opts methods" do 33 | before do 34 | FooModel.db_charmer_opts = nil 35 | end 36 | 37 | it "should implement both accessor methods" do 38 | opts = { :foo => :bar} 39 | FooModel.db_charmer_opts = opts 40 | FooModel.db_charmer_opts.should be(opts) 41 | end 42 | end 43 | 44 | context "in db_charmer_slaves methods" do 45 | it "should return [] if no slaves set for a model" do 46 | FooModel.db_charmer_slaves = nil 47 | FooModel.db_charmer_slaves.should == [] 48 | end 49 | 50 | it "should implement both accessor methods" do 51 | proxy = double('connection proxy') 52 | FooModel.db_charmer_slaves = [ proxy ] 53 | FooModel.db_charmer_slaves.should == [ proxy ] 54 | end 55 | 56 | it "should implement random slave selection" do 57 | FooModel.db_charmer_slaves = [ :proxy1, :proxy2, :proxy3 ] 58 | srand(0) 59 | FooModel.db_charmer_random_slave.should == :proxy1 60 | FooModel.db_charmer_random_slave.should == :proxy2 61 | FooModel.db_charmer_random_slave.should == :proxy1 62 | FooModel.db_charmer_random_slave.should == :proxy2 63 | FooModel.db_charmer_random_slave.should == :proxy2 64 | FooModel.db_charmer_random_slave.should == :proxy3 65 | end 66 | end 67 | 68 | context "in db_charmer_connection_levels methods" do 69 | it "should return 0 by default" do 70 | FooModel.db_charmer_connection_level = nil 71 | FooModel.db_charmer_connection_level.should == 0 72 | end 73 | 74 | it "should implement both accessor methods and support inc/dec operations" do 75 | FooModel.db_charmer_connection_level = 1 76 | FooModel.db_charmer_connection_level.should == 1 77 | FooModel.db_charmer_connection_level += 1 78 | FooModel.db_charmer_connection_level.should == 2 79 | FooModel.db_charmer_connection_level -= 1 80 | FooModel.db_charmer_connection_level.should == 1 81 | end 82 | 83 | it "should implement db_charmer_top_level_connection? method" do 84 | FooModel.db_charmer_connection_level = 1 85 | FooModel.should_not be_db_charmer_top_level_connection 86 | FooModel.db_charmer_connection_level = 0 87 | FooModel.should be_db_charmer_top_level_connection 88 | end 89 | end 90 | 91 | context "in connection method" do 92 | it "should return AR's original connection if no connection proxy is set" do 93 | FooModel.db_charmer_connection_proxy = nil 94 | FooModel.db_charmer_default_connection = nil 95 | FooModel.connection.should be_kind_of(ActiveRecord::ConnectionAdapters::AbstractAdapter) 96 | end 97 | end 98 | 99 | context "in db_charmer_force_slave_reads? method" do 100 | it "should use per-model settings when possible" do 101 | FooModel.db_charmer_force_slave_reads = true 102 | DbCharmer.should_not_receive(:force_slave_reads?) 103 | FooModel.db_charmer_force_slave_reads?.should be_true 104 | end 105 | 106 | it "should use global settings when local setting is false" do 107 | FooModel.db_charmer_force_slave_reads = false 108 | 109 | DbCharmer.should_receive(:force_slave_reads?).and_return(true) 110 | FooModel.db_charmer_force_slave_reads?.should be_true 111 | 112 | DbCharmer.should_receive(:force_slave_reads?).and_return(false) 113 | FooModel.db_charmer_force_slave_reads?.should be_false 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/connection_switching_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class FooModelForConnSwitching < ActiveRecord::Base; end 4 | class BarModelForConnSwitching < ActiveRecord::Base; end 5 | 6 | describe DbCharmer, "AR connection switching" do 7 | describe "in switch_connection_to method" do 8 | before(:all) do 9 | BarModelForConnSwitching.hijack_connection! 10 | end 11 | 12 | before :each do 13 | @proxy = double('proxy') 14 | @proxy.stub(:db_charmer_connection_name).and_return(:myproxy) 15 | end 16 | 17 | before do 18 | BarModelForConnSwitching.db_charmer_connection_proxy = @proxy 19 | BarModelForConnSwitching.connection.should be(@proxy) 20 | end 21 | 22 | it "should accept nil and reset connection to default" do 23 | BarModelForConnSwitching.switch_connection_to(nil) 24 | BarModelForConnSwitching.connection.should be(ActiveRecord::Base.connection) 25 | end 26 | 27 | it "should accept a string and generate an abstract class with connection factory" do 28 | BarModelForConnSwitching.switch_connection_to('logs') 29 | BarModelForConnSwitching.connection.object_id == DbCharmer::ConnectionFactory.connect('logs').object_id 30 | end 31 | 32 | it "should accept a symbol and generate an abstract class with connection factory" do 33 | BarModelForConnSwitching.switch_connection_to(:logs) 34 | BarModelForConnSwitching.connection.object_id.should == DbCharmer::ConnectionFactory.connect('logs').object_id 35 | end 36 | 37 | it "should accept a model and use its connection proxy value" do 38 | FooModelForConnSwitching.switch_connection_to(:logs) 39 | BarModelForConnSwitching.switch_connection_to(FooModelForConnSwitching) 40 | BarModelForConnSwitching.connection.object_id.should == DbCharmer::ConnectionFactory.connect('logs').object_id 41 | end 42 | 43 | context "with a hash parameter" do 44 | before do 45 | @conf = { 46 | :adapter => 'mysql', 47 | :username => "db_charmer_ro", 48 | :database => "db_charmer_sandbox_test", 49 | :connection_name => 'sanbox_ro' 50 | } 51 | end 52 | 53 | it "should fail if there is no :connection_name parameter" do 54 | @conf.delete(:connection_name) 55 | lambda { BarModelForConnSwitching.switch_connection_to(@conf) }.should raise_error(ArgumentError) 56 | end 57 | 58 | it "generate an abstract class with connection factory" do 59 | BarModelForConnSwitching.switch_connection_to(@conf) 60 | BarModelForConnSwitching.connection.object_id.should == DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf).object_id 61 | end 62 | end 63 | 64 | it "should support connection switching for AR::Base" do 65 | ActiveRecord::Base.switch_connection_to(:logs) 66 | ActiveRecord::Base.connection.object_id == DbCharmer::ConnectionFactory.connect('logs').object_id 67 | ActiveRecord::Base.switch_connection_to(nil) 68 | end 69 | end 70 | end 71 | 72 | describe DbCharmer, "for ActiveRecord models" do 73 | describe "in establish_real_connection_if_exists method" do 74 | it "should check connection name if requested" do 75 | lambda { FooModelForConnSwitching.establish_real_connection_if_exists(:foo, true) }.should raise_error(ArgumentError) 76 | end 77 | 78 | it "should not check connection name if not reqested" do 79 | lambda { FooModelForConnSwitching.establish_real_connection_if_exists(:foo) }.should_not raise_error 80 | end 81 | 82 | it "should not check connection name if reqested not to" do 83 | lambda { FooModelForConnSwitching.establish_real_connection_if_exists(:foo, false) }.should_not raise_error 84 | end 85 | 86 | it "should establish connection when connection configuration exists" do 87 | FooModelForConnSwitching.should_receive(:establish_connection) 88 | FooModelForConnSwitching.establish_real_connection_if_exists(:logs) 89 | end 90 | 91 | it "should not establish connection even when connection configuration does not exist" do 92 | FooModelForConnSwitching.should_not_receive(:establish_connection) 93 | FooModelForConnSwitching.establish_real_connection_if_exists(:blah) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/db_magic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class Blah < ActiveRecord::Base; end 4 | 5 | describe "In ActiveRecord models" do 6 | describe "db_magic method" do 7 | context "with :connection parameter" do 8 | after do 9 | DbCharmer.connections_should_exist = false 10 | end 11 | 12 | it "should change model's connection to specified one" do 13 | Blah.db_magic :connection => :logs 14 | Blah.connection.object_id.should == DbCharmer::ConnectionFactory.connect(:logs).object_id 15 | end 16 | 17 | it "should pass :should_exist paramater value to the underlying connection logic" do 18 | DbCharmer::ConnectionFactory.should_receive(:connect).with(:logs, 'blah') 19 | Blah.db_magic :connection => :logs, :should_exist => 'blah' 20 | DbCharmer.connections_should_exist = true 21 | DbCharmer::ConnectionFactory.should_receive(:connect).with(:logs, false) 22 | Blah.db_magic :connection => :logs, :should_exist => false 23 | end 24 | 25 | it "should use global DbCharmer's connections_should_exist attribute if no :should_exist passed" do 26 | DbCharmer.connections_should_exist = true 27 | DbCharmer::ConnectionFactory.should_receive(:connect).with(:logs, true) 28 | Blah.db_magic :connection => :logs 29 | end 30 | end 31 | 32 | context "with :slave or :slaves parameter" do 33 | it "should merge :slave and :slaves values" do 34 | Blah.db_charmer_slaves = [] 35 | Blah.db_charmer_slaves.should be_empty 36 | 37 | Blah.db_magic :slave => :slave01 38 | Blah.db_charmer_slaves.size.should == 1 39 | 40 | Blah.db_magic :slaves => [ :slave01 ] 41 | Blah.db_charmer_slaves.size.should == 1 42 | 43 | Blah.db_magic :slaves => [ :slave01 ], :slave => :logs 44 | Blah.db_charmer_slaves.size.should == 2 45 | end 46 | 47 | it "should make db_charmer_force_slave_reads = true by default" do 48 | Blah.db_magic :slave => :slave01 49 | Blah.db_charmer_force_slave_reads.should be_true 50 | end 51 | 52 | it "should pass force_slave_reads value to db_charmer_force_slave_reads" do 53 | Blah.db_magic :slave => :slave01, :force_slave_reads => false 54 | Blah.db_charmer_force_slave_reads.should be_false 55 | 56 | Blah.db_magic :slave => :slave01, :force_slave_reads => true 57 | Blah.db_charmer_force_slave_reads.should be_true 58 | end 59 | end 60 | 61 | it "should set up a hook to propagate db_magic params to all the children models" do 62 | class ParentFoo < ActiveRecord::Base 63 | db_magic :foo => :bar 64 | end 65 | class ChildFoo < ParentFoo; end 66 | 67 | ChildFoo.db_charmer_opts.should == ParentFoo.db_charmer_opts 68 | end 69 | 70 | context "with :sharded parameter" do 71 | class ShardTestingFoo < ActiveRecord::Base 72 | db_magic :sharded => { :key => :id, :sharded_connection => :texts } 73 | end 74 | 75 | it "should add shard_for method to the model" do 76 | ShardTestingFoo.should respond_to(:shard_for) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/master_slave_routing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ActiveRecord slave-enabled models" do 4 | before do 5 | class User < ActiveRecord::Base 6 | db_magic :connection => :user_master, :slave => :slave01 7 | end 8 | end 9 | 10 | describe "in finder method" do 11 | [ :last, :first, :all ].each do |meth| 12 | describe meth do 13 | it "should go to the slave if called on the first level connection" do 14 | User.on_slave.connection.should_receive(:select_all).and_return([]) 15 | User.send(meth) 16 | end 17 | 18 | it "should not change connection if called in an on_db block" do 19 | stub_columns_for_rails31 User.on_db(:logs).connection 20 | User.on_db(:logs).connection.should_receive(:select_all).and_return([]) 21 | User.on_slave.connection.should_not_receive(:select_all) 22 | User.on_db(:logs).send(meth) 23 | end 24 | 25 | it "should not change connection when it's already been changed by on_slave call" do 26 | pending "rails3: not sure if we need this spec" if DbCharmer.rails3? 27 | User.on_slave do 28 | User.on_slave.connection.should_receive(:select_all).and_return([]) 29 | User.should_not_receive(:on_db) 30 | User.send(meth) 31 | end 32 | end 33 | 34 | it "should not change connection if called in a transaction" do 35 | User.on_db(:user_master).connection.should_receive(:select_all).and_return([]) 36 | User.on_slave.connection.should_not_receive(:select_all) 37 | User.transaction { User.send(meth) } 38 | end 39 | end 40 | end 41 | 42 | it "should go to the master if called find with :lock => true option" do 43 | User.on_db(:user_master).connection.should_receive(:select_all).and_return([]) 44 | User.on_slave.connection.should_not_receive(:select_all) 45 | User.find(:first, :lock => true) 46 | end 47 | 48 | it "should not go to the master if no :lock => true option passed" do 49 | User.on_db(:user_master).connection.should_not_receive(:select_all) 50 | User.on_slave.connection.should_receive(:select_all).and_return([]) 51 | User.find(:first) 52 | end 53 | 54 | it "should correctly pass all find params to the underlying code" do 55 | User.delete_all 56 | u1 = User.create(:login => 'foo') 57 | u2 = User.create(:login => 'bar') 58 | 59 | User.find(:all, :conditions => { :login => 'foo' }).should == [ u1 ] 60 | User.find(:all, :limit => 1).size.should == 1 61 | User.find(:first, :conditions => { :login => 'bar' }).should == u2 62 | end 63 | end 64 | 65 | describe "in calculation method" do 66 | [ :count, :minimum, :maximum, :average ].each do |meth| 67 | describe meth do 68 | it "should go to the slave if called on the first level connection" do 69 | User.on_slave.connection.should_receive(:select_value).and_return(1) 70 | User.send(meth, :id).should == 1 71 | end 72 | 73 | it "should not change connection if called in an on_db block" do 74 | User.on_db(:logs).connection.should_receive(:select_value).and_return(1) 75 | User.on_slave.connection.should_not_receive(:select_value) 76 | User.on_db(:logs).send(meth, :id).should == 1 77 | end 78 | 79 | it "should not change connection when it's already been changed by an on_slave call" do 80 | pending "rails3: not sure if we need this spec" if DbCharmer.rails3? 81 | User.on_slave do 82 | User.on_slave.connection.should_receive(:select_value).and_return(1) 83 | User.should_not_receive(:on_db) 84 | User.send(meth, :id).should == 1 85 | end 86 | end 87 | 88 | it "should not change connection if called in a transaction" do 89 | User.on_db(:user_master).connection.should_receive(:select_value).and_return(1) 90 | User.on_slave.connection.should_not_receive(:select_value) 91 | User.transaction { User.send(meth, :id).should == 1 } 92 | end 93 | end 94 | end 95 | end 96 | 97 | describe "in data manipulation methods" do 98 | it "should go to the master by default" do 99 | User.on_db(:user_master).connection.should_receive(:delete) 100 | User.delete_all 101 | end 102 | 103 | it "should go to the master even in slave-enabling chain calls" do 104 | User.on_db(:user_master).connection.should_receive(:delete) 105 | User.on_slave.delete_all 106 | end 107 | 108 | it "should go to the master even in slave-enabling block calls" do 109 | User.on_db(:user_master).connection.should_receive(:delete) 110 | User.on_slave { |u| u.delete_all } 111 | end 112 | end 113 | 114 | describe "in instance method" do 115 | describe "reload" do 116 | it "should always be done on the master" do 117 | User.delete_all 118 | u = User.create 119 | 120 | User.on_db(:user_master).connection.should_receive(:select_all).and_return([{}]) 121 | User.on_slave.connection.should_not_receive(:select_all) 122 | 123 | User.on_slave { u.reload } 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/named_scope/named_scope_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Named scopes" do 4 | fixtures :users, :posts 5 | 6 | before(:all) do 7 | Post.switch_connection_to(nil) 8 | User.switch_connection_to(nil) 9 | end 10 | 11 | describe "prefixed by on_db" do 12 | it "should work on the proxy" do 13 | Post.on_db(:slave01).windows_posts.should == Post.windows_posts 14 | end 15 | 16 | it "should actually run queries on the specified db" do 17 | Post.on_db(:slave01).connection.should_receive(:select_all).once.and_return([]) 18 | Post.on_db(:slave01).windows_posts.all 19 | # Post.windows_posts.all 20 | end 21 | 22 | it "should work with long scope chains" do 23 | Post.on_db(:slave01).connection.should_not_receive(:select_all) 24 | Post.on_db(:slave01).connection.should_receive(:select_value).and_return(5) 25 | Post.on_db(:slave01).windows_posts.count.should == 5 26 | end 27 | 28 | it "should work with associations" do 29 | users(:bill).posts.on_db(:slave01).windows_posts.all.should == users(:bill).posts.windows_posts 30 | end 31 | end 32 | 33 | describe "postfixed by on_db" do 34 | it "should work on the proxy" do 35 | Post.windows_posts.on_db(:slave01).should == Post.windows_posts 36 | end 37 | 38 | it "should actually run queries on the specified db" do 39 | Post.on_db(:slave01).connection.object_id.should_not == Post.connection.object_id 40 | Post.on_db(:slave01).connection.should_receive(:select_all).and_return([]) 41 | Post.windows_posts.on_db(:slave01).all 42 | Post.windows_posts.all 43 | end 44 | 45 | it "should work with long scope chains" do 46 | Post.on_db(:slave01).connection.should_not_receive(:select_all) 47 | Post.on_db(:slave01).connection.should_receive(:select_value).and_return(5) 48 | Post.windows_posts.on_db(:slave01).count.should == 5 49 | end 50 | 51 | it "should work with associations" do 52 | users(:bill).posts.windows_posts.on_db(:slave01).all.should == users(:bill).posts.windows_posts 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test-project/spec/unit/active_record/relation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if DbCharmer.rails3? 4 | describe "ActiveRecord::Relation for a model with db_magic" do 5 | before do 6 | class RelTestModel < ActiveRecord::Base 7 | db_magic :connection => nil 8 | self.table_name = :users 9 | end 10 | end 11 | 12 | it "should be created with correct default connection" do 13 | rel = RelTestModel.on_db(:user_master).where("1=1") 14 | rel.db_charmer_connection.object_id.should == RelTestModel.on_db(:user_master).connection.object_id 15 | end 16 | 17 | it "should switch the default connection when on_db called" do 18 | rel = RelTestModel.where("1=1") 19 | rel_master = rel.on_db(:user_master) 20 | rel_master.db_charmer_connection.object_id.should_not == rel.db_charmer_connection.object_id 21 | end 22 | 23 | it "should keep default connection value when relation is cloned in chained calls" do 24 | rel = RelTestModel.on_db(:user_master).where("1=1") 25 | rel.where("2=2").db_charmer_connection.object_id.should == rel.db_charmer_connection.object_id 26 | end 27 | 28 | it "should execute select queries on the default connection" do 29 | rel = RelTestModel.on_db(:user_master).where("1=1") 30 | 31 | RelTestModel.on_db(:user_master).connection.should_receive(:select_all).and_return([]) 32 | RelTestModel.connection.should_not_receive(:select_all) 33 | 34 | rel.first 35 | end 36 | 37 | it "should execute delete queries on the default connection" do 38 | rel = RelTestModel.on_db(:user_master).where("1=1") 39 | 40 | RelTestModel.on_db(:user_master).connection.should_receive(:delete) 41 | RelTestModel.connection.should_not_receive(:delete) 42 | 43 | rel.delete_all 44 | end 45 | 46 | it "should execute update_all queries on the default connection" do 47 | rel = RelTestModel.on_db(:user_master).where("1=1") 48 | 49 | RelTestModel.on_db(:user_master).connection.should_receive(:update) 50 | RelTestModel.connection.should_not_receive(:update) 51 | 52 | rel.update_all("login = login + 'new'") 53 | end 54 | 55 | it "should execute update queries on the default connection" do 56 | rel = RelTestModel.on_db(:user_master).where("1=1") 57 | user = RelTestModel.create!(:login => 'login') 58 | 59 | RelTestModel.on_db(:user_master).connection.should_receive(:update) 60 | RelTestModel.connection.should_not_receive(:update) 61 | 62 | rel.update(user.id, :login => "foobar") 63 | end 64 | 65 | it "should return correct connection" do 66 | rel = RelTestModel.on_db(:user_master).where("1=1") 67 | rel.connection.object_id.should == rel.db_charmer_connection.object_id 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test-project/spec/unit/connection_factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer::ConnectionFactory do 4 | context "in generate_abstract_class method" do 5 | it "should fail if requested connection config does not exists" do 6 | lambda { DbCharmer::ConnectionFactory.generate_abstract_class('foo') }.should raise_error(ArgumentError) 7 | end 8 | 9 | it "should not fail if requested connection config does not exists and should_exist = false" do 10 | lambda { DbCharmer::ConnectionFactory.generate_abstract_class('foo', false) }.should_not raise_error 11 | end 12 | 13 | it "should fail if requested connection config does not exists and should_exist = true" do 14 | lambda { DbCharmer::ConnectionFactory.generate_abstract_class('foo', true) }.should raise_error(ArgumentError) 15 | end 16 | 17 | it "should generate abstract connection classes" do 18 | klass = DbCharmer::ConnectionFactory.generate_abstract_class('foo', false) 19 | klass.superclass.should be(ActiveRecord::Base) 20 | end 21 | 22 | it "should work with weird connection names" do 23 | klass = DbCharmer::ConnectionFactory.generate_abstract_class('foo.bar@baz#blah', false) 24 | klass.superclass.should be(ActiveRecord::Base) 25 | end 26 | end 27 | 28 | context "in generate_empty_abstract_ar_class method" do 29 | it "should generate an abstract connection class" do 30 | klass = DbCharmer::ConnectionFactory.generate_empty_abstract_ar_class('::MyFooAbstractClass') 31 | klass.superclass.should be(ActiveRecord::Base) 32 | end 33 | end 34 | 35 | context "in establish_connection method" do 36 | it "should generate an abstract class" do 37 | klass = mock('AbstractClass') 38 | conn = mock('connection1') 39 | klass.stub!(:retrieve_connection).and_return(conn) 40 | DbCharmer::ConnectionFactory.should_receive(:generate_abstract_class).and_return(klass) 41 | DbCharmer::ConnectionFactory.establish_connection(:foo).should be(conn) 42 | end 43 | 44 | it "should create and return a connection proxy for the abstract class" do 45 | klass = mock('AbstractClass') 46 | DbCharmer::ConnectionFactory.should_receive(:generate_abstract_class).and_return(klass) 47 | DbCharmer::ConnectionProxy.should_receive(:new).with(klass, :foo) 48 | DbCharmer::ConnectionFactory.establish_connection(:foo) 49 | end 50 | end 51 | 52 | context "in establish_connection_to_db method" do 53 | it "should generate an abstract class" do 54 | klass = mock('AbstractClass') 55 | conn = mock('connection2') 56 | klass.stub!(:establish_connection) 57 | klass.stub!(:retrieve_connection).and_return(conn) 58 | DbCharmer::ConnectionFactory.should_receive(:generate_empty_abstract_ar_class).and_return(klass) 59 | DbCharmer::ConnectionFactory.establish_connection_to_db(:foo, :username => :foo).should be(conn) 60 | end 61 | 62 | it "should create and return a connection proxy for the abstract class" do 63 | klass = mock('AbstractClass') 64 | klass.stub!(:establish_connection) 65 | DbCharmer::ConnectionFactory.should_receive(:generate_empty_abstract_ar_class).and_return(klass) 66 | DbCharmer::ConnectionProxy.should_receive(:new).with(klass, :foo) 67 | DbCharmer::ConnectionFactory.establish_connection_to_db(:foo, :username => :foo) 68 | end 69 | end 70 | 71 | context "in connect method" do 72 | before do 73 | DbCharmer::ConnectionFactory.reset! 74 | end 75 | 76 | it "should return a connection proxy" do 77 | DbCharmer::ConnectionFactory.connect(:logs).should be_kind_of(ActiveRecord::ConnectionAdapters::AbstractAdapter) 78 | end 79 | 80 | # should_receive is evil on a singletone classes 81 | # it "should memoize proxies" do 82 | # conn = mock('connection3') 83 | # DbCharmer::ConnectionFactory.should_receive(:establish_connection).with('foo', false).once.and_return(conn) 84 | # DbCharmer::ConnectionFactory.connect(:foo) 85 | # DbCharmer::ConnectionFactory.connect(:foo) 86 | # end 87 | end 88 | 89 | context "in connect_to_db method" do 90 | before do 91 | DbCharmer::ConnectionFactory.reset! 92 | @conf = { 93 | :adapter => 'mysql', 94 | :username => "db_charmer_ro", 95 | :database => "db_charmer_sandbox_test", 96 | :connection_name => 'sanbox_ro' 97 | } 98 | end 99 | 100 | it "should return a connection proxy" do 101 | DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf).should be_kind_of(ActiveRecord::ConnectionAdapters::AbstractAdapter) 102 | end 103 | 104 | # should_receive is evil on a singletone classes 105 | # it "should memoize proxies" do 106 | # conn = mock('connection4') 107 | # DbCharmer::ConnectionFactory.should_receive(:establish_connection_to_db).with(@conf[:connection_name], @conf).once.and_return(conn) 108 | # DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf) 109 | # DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf) 110 | # end 111 | end 112 | 113 | end 114 | -------------------------------------------------------------------------------- /test-project/spec/unit/connection_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer::ConnectionProxy do 4 | before(:each) do 5 | class ProxyTest; end 6 | @conn = mock('connection') 7 | @proxy = DbCharmer::ConnectionProxy.new(ProxyTest, :foo) 8 | end 9 | 10 | it "should retrieve connection from an underlying class" do 11 | ProxyTest.should_receive(:retrieve_connection).and_return(@conn) 12 | @proxy.inspect 13 | end 14 | 15 | it "should be a blankslate for the connection" do 16 | ProxyTest.stub!(:retrieve_connection).and_return(@conn) 17 | @proxy.should be(@conn) 18 | end 19 | 20 | it "should proxy methods with a block parameter" do 21 | module MockConnection 22 | def self.foo 23 | raise "No block given!" unless block_given? 24 | yield 25 | end 26 | end 27 | ProxyTest.stub!(:retrieve_connection).and_return(MockConnection) 28 | res = @proxy.foo { :foo } 29 | res.should == :foo 30 | end 31 | 32 | it "should proxy all calls to the underlying class connections" do 33 | ProxyTest.stub!(:retrieve_connection).and_return(@conn) 34 | @conn.should_receive(:foo) 35 | @proxy.foo 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test-project/spec/unit/db_charmer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DbCharmer do 4 | after do 5 | DbCharmer.current_controller = nil 6 | DbCharmer.connections_should_exist = false 7 | end 8 | 9 | it "should define version constants" do 10 | DbCharmer::Version::STRING.should match(/^\d+\.\d+\.\d+/) 11 | end 12 | 13 | it "should have connections_should_exist accessors" do 14 | DbCharmer.connections_should_exist.should_not be_nil 15 | DbCharmer.connections_should_exist = :foo 16 | DbCharmer.connections_should_exist.should == :foo 17 | end 18 | 19 | it "should have connections_should_exist? method" do 20 | DbCharmer.connections_should_exist = true 21 | DbCharmer.connections_should_exist?.should be_true 22 | DbCharmer.connections_should_exist = false 23 | DbCharmer.connections_should_exist?.should be_false 24 | DbCharmer.connections_should_exist = "shit" 25 | DbCharmer.connections_should_exist?.should be_true 26 | DbCharmer.connections_should_exist = nil 27 | DbCharmer.connections_should_exist?.should be_false 28 | end 29 | 30 | it "should have current_controller accessors" do 31 | DbCharmer.respond_to?(:current_controller).should be_true 32 | DbCharmer.current_controller = :foo 33 | DbCharmer.current_controller.should == :foo 34 | DbCharmer.current_controller = nil 35 | end 36 | 37 | context "in force_slave_reads? method" do 38 | it "should return true if force_slave_reads=true" do 39 | DbCharmer.force_slave_reads?.should be_false 40 | 41 | DbCharmer.force_slave_reads do 42 | DbCharmer.force_slave_reads?.should be_true 43 | end 44 | 45 | DbCharmer.force_slave_reads?.should be_false 46 | end 47 | 48 | it "should return false if no controller defined and global force_slave_reads=false" do 49 | DbCharmer.current_controller = nil 50 | DbCharmer.force_slave_reads?.should be_false 51 | end 52 | 53 | it "should consult with the controller about forcing slave reads if possible" do 54 | DbCharmer.current_controller = mock("controller") 55 | 56 | DbCharmer.current_controller.should_receive(:force_slave_reads?).and_return(true) 57 | DbCharmer.force_slave_reads?.should be_true 58 | 59 | DbCharmer.current_controller.should_receive(:force_slave_reads?).and_return(false) 60 | DbCharmer.force_slave_reads?.should be_false 61 | end 62 | end 63 | 64 | context "in with_controller method" do 65 | it "should fail if no block given" do 66 | lambda { DbCharmer.with_controller(:foo) }.should raise_error(ArgumentError) 67 | end 68 | 69 | it "should switch controller while running the block" do 70 | DbCharmer.current_controller = nil 71 | DbCharmer.current_controller.should be_nil 72 | 73 | DbCharmer.with_controller(:foo) do 74 | DbCharmer.current_controller.should == :foo 75 | end 76 | 77 | DbCharmer.current_controller.should be_nil 78 | end 79 | 80 | it "should ensure current controller is reverted to nil in case of errors" do 81 | lambda { 82 | DbCharmer.with_controller(:foo) { raise "fuck" } 83 | }.should raise_error 84 | DbCharmer.current_controller.should be_nil 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test-project/spec/unit/multi_db_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "ActiveRecord model with db_magic" do 4 | before do 5 | class Blah < ActiveRecord::Base 6 | self.table_name = :posts 7 | db_magic :connection => nil 8 | end 9 | end 10 | 11 | describe "(instance)" do 12 | before do 13 | @blah = Blah.new 14 | end 15 | 16 | describe "in on_db method" do 17 | describe "with a block" do 18 | it "should switch connection to specified one and yield the block" do 19 | Blah.db_charmer_connection_proxy.should be_nil 20 | @blah.on_db(:logs) do 21 | Blah.db_charmer_connection_proxy.should_not be_nil 22 | end 23 | end 24 | 25 | it "should switch connection back after the block finished its work" do 26 | Blah.db_charmer_connection_proxy.should be_nil 27 | @blah.on_db(:logs) {} 28 | Blah.db_charmer_connection_proxy.should be_nil 29 | end 30 | 31 | it "should manage connection level values" do 32 | Blah.db_charmer_connection_level.should == 0 33 | @blah.on_db(:logs) do |m| 34 | m.class.db_charmer_connection_level.should == 1 35 | end 36 | Blah.db_charmer_connection_level.should == 0 37 | end 38 | end 39 | 40 | describe "as a chain call" do 41 | it "should switch connection for all chained calls" do 42 | Blah.db_charmer_connection_proxy.should be_nil 43 | @blah.on_db(:logs).should_not be_nil 44 | end 45 | 46 | it "should switch connection for non-chained calls" do 47 | Blah.db_charmer_connection_proxy.should be_nil 48 | @blah.on_db(:logs).to_s 49 | Blah.db_charmer_connection_proxy.should be_nil 50 | end 51 | 52 | it "should restore connection" do 53 | User.first 54 | User.connection.object_id.should == User.on_master.connection.object_id 55 | 56 | User.on_db(:slave01).first 57 | User.connection.object_id.should == User.on_master.connection.object_id 58 | end 59 | 60 | it "should restore connection after error" do 61 | pending "Disabled in RSpec prior to version 2 because of lack of .any_instance support" unless Object.respond_to?(:any_instance) 62 | 63 | User.on_db(:slave01).first 64 | User.first 65 | ActiveRecord::Base.connection_handler.clear_all_connections! 66 | ActiveRecord::ConnectionAdapters::MysqlAdapter.any_instance.stub(:connect) { raise Mysql::Error, 'Connection error' } 67 | expect { User.on_db(:slave01).first }.to raise_error(Mysql::Error) 68 | ActiveRecord::ConnectionAdapters::MysqlAdapter.any_instance.unstub(:connect) 69 | User.connection.connection_name.should == User.on_master.connection.connection_name 70 | end 71 | end 72 | end 73 | end 74 | 75 | describe "(class)" do 76 | describe "in on_db method" do 77 | describe "with a block" do 78 | it "should switch connection to specified one and yield the block" do 79 | Blah.db_charmer_connection_proxy.should be_nil 80 | Blah.on_db(:logs) do 81 | Blah.db_charmer_connection_proxy.should_not be_nil 82 | end 83 | end 84 | 85 | it "should switch connection back after the block finished its work" do 86 | Blah.db_charmer_connection_proxy.should be_nil 87 | Blah.on_db(:logs) {} 88 | Blah.db_charmer_connection_proxy.should be_nil 89 | end 90 | 91 | it "should manage connection level values" do 92 | Blah.db_charmer_connection_level.should == 0 93 | Blah.on_db(:logs) do |m| 94 | m.db_charmer_connection_level.should == 1 95 | end 96 | Blah.db_charmer_connection_level.should == 0 97 | end 98 | end 99 | 100 | describe "as a chain call" do 101 | it "should switch connection for all chained calls" do 102 | Blah.db_charmer_connection_proxy.should be_nil 103 | Blah.on_db(:logs).should_not be_nil 104 | end 105 | 106 | it "should switch connection for non-chained calls" do 107 | Blah.db_charmer_connection_proxy.should be_nil 108 | Blah.on_db(:logs).to_s 109 | Blah.db_charmer_connection_proxy.should be_nil 110 | end 111 | end 112 | end 113 | 114 | describe "in on_slave method" do 115 | before do 116 | Blah.db_magic :slaves => [ :slave01 ] 117 | end 118 | 119 | it "should use one tof the model's slaves if no slave given" do 120 | Blah.on_slave.db_charmer_connection_proxy.object_id.should == Blah.coerce_to_connection_proxy(:slave01).object_id 121 | end 122 | 123 | it "should use given slave" do 124 | Blah.on_slave(:logs).db_charmer_connection_proxy.object_id.should == Blah.coerce_to_connection_proxy(:logs).object_id 125 | end 126 | 127 | it 'should support block calls' do 128 | Blah.on_slave do |m| 129 | m.db_charmer_connection_proxy.object_id.should == Blah.coerce_to_connection_proxy(:slave01).object_id 130 | end 131 | end 132 | end 133 | 134 | describe "in on_master method" do 135 | before do 136 | Blah.db_magic :slaves => [ :slave01 ] 137 | end 138 | 139 | it "should run queries on the master" do 140 | Blah.on_master.db_charmer_connection_proxy.should be_nil 141 | end 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /test-project/spec/unit/with_remapped_databases_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "DbCharmer#with_remapped_databases" do 4 | before(:all) do 5 | DbCharmer.connections_should_exist = false 6 | end 7 | 8 | let(:logs_connection) { DbCharmer::ConnectionFactory.connect(:logs) } 9 | let(:slave_connection) { DbCharmer::ConnectionFactory.connect(:slave01) } 10 | let(:master_connection) { Avatar.connection } 11 | 12 | before :each do 13 | class User < ActiveRecord::Base 14 | db_magic :connection => :slave01 15 | end 16 | end 17 | 18 | def should_have_connection(model_class, connection) 19 | model_class.connection.object_id.should == connection.object_id 20 | end 21 | 22 | it "should remap the right connection" do 23 | should_have_connection(LogRecord, logs_connection) 24 | DbCharmer.with_remapped_databases(:logs => :slave01) do 25 | should_have_connection(LogRecord, slave_connection) 26 | end 27 | should_have_connection(LogRecord, logs_connection) 28 | end 29 | 30 | it "should not remap other connections" do 31 | should_have_connection(Avatar, master_connection) 32 | should_have_connection(User, slave_connection) 33 | DbCharmer.with_remapped_databases(:logs => :slave01) do 34 | should_have_connection(Avatar, master_connection) 35 | should_have_connection(User, slave_connection) 36 | end 37 | should_have_connection(Avatar, master_connection) 38 | should_have_connection(User, slave_connection) 39 | end 40 | 41 | it "should allow remapping multiple databases" do 42 | should_have_connection(Avatar, master_connection) 43 | should_have_connection(LogRecord, logs_connection) 44 | DbCharmer.with_remapped_databases(:master => :logs, :logs => :slave01) do 45 | should_have_connection(Avatar, logs_connection) 46 | should_have_connection(LogRecord, slave_connection) 47 | end 48 | should_have_connection(Avatar, master_connection) 49 | should_have_connection(LogRecord, logs_connection) 50 | end 51 | 52 | it "should remap the master connection when asked to, but not other connections" do 53 | should_have_connection(Avatar, master_connection) 54 | should_have_connection(User, slave_connection) 55 | should_have_connection(LogRecord, logs_connection) 56 | DbCharmer.with_remapped_databases(:master => :slave01) do 57 | should_have_connection(Avatar, slave_connection) 58 | should_have_connection(User, slave_connection) 59 | should_have_connection(LogRecord, logs_connection) 60 | end 61 | should_have_connection(Avatar, master_connection) 62 | should_have_connection(User, slave_connection) 63 | should_have_connection(LogRecord, logs_connection) 64 | end 65 | 66 | it "should not override connections that are explicitly specified" do 67 | DbCharmer.with_remapped_databases(:logs => :slave01) do 68 | should_have_connection(LogRecord, slave_connection) 69 | should_have_connection(LogRecord.on_db(:master), master_connection) 70 | LogRecord.on_db(:master) do 71 | should_have_connection(LogRecord, master_connection) 72 | end 73 | should_have_connection(LogRecord.on_db(:logs), logs_connection) 74 | LogRecord.on_db(:logs) do 75 | should_have_connection(LogRecord, logs_connection) 76 | end 77 | should_have_connection(LogRecord, slave_connection) 78 | end 79 | end 80 | 81 | it "should successfully run selects on the right database" do 82 | # We need this call to make sure rails would fetch columns info from the logs server before we mess its connection up 83 | LogRecord.all 84 | 85 | # Remap LogRecord connection to slave01 and make sure selects would go there (even though we do not have the table there) 86 | DbCharmer.with_remapped_databases(:logs => :slave01) do 87 | logs_connection.should_not_receive(:select_all) 88 | slave_connection.should_receive(:select_all).and_return([]) 89 | stub_columns_for_rails31 slave_connection 90 | LogRecord.all.should be_empty 91 | end 92 | end 93 | 94 | def unhijack!(klass) 95 | if klass.respond_to?(:connection_with_magic) 96 | klass.class_eval <<-END 97 | class << self 98 | undef_method(:connection_with_magic) 99 | alias_method(:connection, :connection_without_magic) 100 | undef_method(:connection_without_magic) 101 | 102 | undef_method(:connection_pool_with_magic) 103 | alias_method(:connection_pool, :connection_pool_without_magic) 104 | undef_method(:connection_pool_without_magic) 105 | end 106 | END 107 | end 108 | 109 | raise "Unable to unhijack #{klass.name}" if klass.respond_to?(:connection_with_magic) 110 | end 111 | 112 | it "should hijack connections only when necessary" do 113 | unhijack!(Category) 114 | 115 | Category.respond_to?(:connection_with_magic).should be_false 116 | DbCharmer.with_remapped_databases(:logs => :slave01) do 117 | Category.respond_to?(:connection_with_magic).should be_false 118 | end 119 | Category.respond_to?(:connection_with_magic).should be_false 120 | 121 | DbCharmer.with_remapped_databases(:master => :slave01) do 122 | Category.respond_to?(:connection_with_magic).should be_true 123 | should_have_connection(Category, slave_connection) 124 | end 125 | end 126 | end 127 | --------------------------------------------------------------------------------