├── .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: {}[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 |
5 | Post #<%= post.id %>
6 |
<%= post.inspect %>7 | 8 | <% end %> 9 | -------------------------------------------------------------------------------- /test-project/app/views/posts/new.html.erb: -------------------------------------------------------------------------------- 1 |
Find me in app/views/posts/new.html.erb
3 | -------------------------------------------------------------------------------- /test-project/app/views/posts/show.html.erb: -------------------------------------------------------------------------------- 1 |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 | --------------------------------------------------------------------------------