dbhijacker 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
data/Gemfile.lock ADDED
@@ -0,0 +1,61 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dbhijacker (0.4.0)
5
+ rails (~> 2.3.14)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actionmailer (2.3.14)
11
+ actionpack (= 2.3.14)
12
+ actionpack (2.3.14)
13
+ activesupport (= 2.3.14)
14
+ rack (~> 1.1.0)
15
+ activerecord (2.3.14)
16
+ activesupport (= 2.3.14)
17
+ activeresource (2.3.14)
18
+ activesupport (= 2.3.14)
19
+ activesupport (2.3.14)
20
+ columnize (0.3.6)
21
+ diff-lcs (1.1.3)
22
+ linecache (0.46)
23
+ rbx-require-relative (> 0.0.4)
24
+ rack (1.1.3)
25
+ rack-test (0.6.1)
26
+ rack (>= 1.0)
27
+ rails (2.3.14)
28
+ actionmailer (= 2.3.14)
29
+ actionpack (= 2.3.14)
30
+ activerecord (= 2.3.14)
31
+ activeresource (= 2.3.14)
32
+ activesupport (= 2.3.14)
33
+ rake (>= 0.8.3)
34
+ rake (0.9.2.2)
35
+ rbx-require-relative (0.0.5)
36
+ rspec (2.8.0)
37
+ rspec-core (~> 2.8.0)
38
+ rspec-expectations (~> 2.8.0)
39
+ rspec-mocks (~> 2.8.0)
40
+ rspec-core (2.8.0)
41
+ rspec-expectations (2.8.0)
42
+ diff-lcs (~> 1.1.2)
43
+ rspec-mocks (2.8.0)
44
+ ruby-debug (0.10.4)
45
+ columnize (>= 0.1)
46
+ ruby-debug-base (~> 0.10.4.0)
47
+ ruby-debug-base (0.10.4)
48
+ linecache (>= 0.3)
49
+ sqlite3 (1.3.5)
50
+
51
+ PLATFORMS
52
+ ruby
53
+
54
+ DEPENDENCIES
55
+ dbhijacker!
56
+ rack (~> 1.1.0)
57
+ rack-test (~> 0.6.1)
58
+ rake (~> 0.9.2)
59
+ rspec (~> 2.8.0)
60
+ ruby-debug (~> 0.10.4)
61
+ sqlite3 (~> 1.3.5)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,67 @@
1
+ Hijacker
2
+ ========
3
+
4
+ One application, multiple client databases. Although customizable, by default uses a combination of database and regular expression matching against the host domain to figure out which database to connect to.
5
+
6
+ Example
7
+ =======
8
+
9
+ class ApplicationController < ActionController::Base
10
+ hijack_connection({
11
+ # First thing it does is look for static routes. If this option
12
+ # exists and returns a string, it'll use that as the database
13
+ :static_routes => Proc.new {
14
+ case RAILS_ENV
15
+ when "development" then "site_development"
16
+ when "test" then "site_test"
17
+ end
18
+ },
19
+ # If it can't find the host in root.databases, it'll try pattern matching.
20
+ # Grabs $1 after a successful match.
21
+ :domain_patterns => [
22
+ /^(.+)\.domain\.com/, /^.+\.(.+)\..+/, /^(.+)\..+/
23
+ ],
24
+ :after_hijack => Proc.new {
25
+ # Classes using acts_as_nested_set load the table info when preloading code in production.
26
+ # This is wrong 'cause at that point AR is connected to the root database.
27
+ Category.reset_column_information
28
+ }
29
+ })
30
+ end
31
+
32
+ For copy/pasters, a shorter version:
33
+
34
+ hijack_connection({
35
+ :static_routes => Proc.new { "site_#{Rails.env}" if !(Rails.env == "production") },
36
+ :domain_patterns => [/^(.+)\.site\.com/, /^.+\.(.+)\..+/, /^(.+)\..+/],
37
+ :after_hijack => Proc.new { Category.reset_column_information }
38
+ })
39
+
40
+ Configuration
41
+ =============
42
+
43
+ Your database.yml needs a "root" connection like so:
44
+
45
+ ...
46
+
47
+ root: &root
48
+ database: root
49
+ <<: *defaults
50
+
51
+ production:
52
+ <<: *root
53
+
54
+ ...
55
+
56
+ Other parts of database.yml will remain the same (development, test) but production
57
+ apps will initially start up on this root database, then hijack when the first connection
58
+ comes in.
59
+
60
+ Running tests
61
+ =============
62
+
63
+ To run the tests, just invoke RSpec:
64
+
65
+ rspec spec
66
+
67
+ Copyright (c) 2012 Michael Xavier, Donald Plummer, Woody Peterson, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ require 'spec/rake/spectask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :spec
7
+
8
+ desc 'Test the hijacker plugin.'
9
+ Spec::Rake::SpecTask.new(:spec) do |t|
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ end
12
+
13
+ desc 'Generate documentation for the hijacker plugin.'
14
+ Rake::RDocTask.new(:rdoc) do |rdoc|
15
+ rdoc.rdoc_dir = 'rdoc'
16
+ rdoc.title = 'Hijacker'
17
+ rdoc.options << '--line-numbers' << '--inline-source'
18
+ rdoc.rdoc_files.include('README')
19
+ rdoc.rdoc_files.include('lib/**/*.rb')
20
+ end
21
+
22
+ begin
23
+ require 'jeweler'
24
+ Jeweler::Tasks.new do |gemspec|
25
+ gemspec.name = "hijacker"
26
+ gemspec.summary = "One application, multiple client databases"
27
+ gemspec.description = "Allows a single Rails appliation to access many different databases"
28
+ gemspec.email = "woody@crystalcommerce.com"
29
+ gemspec.authors = ["Woody Peterson"]
30
+ end
31
+ rescue LoadError
32
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
33
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,21 @@
1
+ ActiveRecord::Schema.define(:version => 2) do
2
+ create_table "databases", :force => true do |t|
3
+ t.string "database"
4
+ t.integer "master_id"
5
+ t.integer "host_id"
6
+ end
7
+
8
+ add_index "databases", "database"
9
+ add_index "databases", "master_id"
10
+
11
+ create_table "aliases", :force => true do |t|
12
+ t.integer "database_id"
13
+ t.string "name"
14
+ end
15
+
16
+ add_index "aliases", "name"
17
+
18
+ create_table "hosts", :force => true do |t|
19
+ t.string "hostname"
20
+ end
21
+ end
data/hijacker.gemspec ADDED
@@ -0,0 +1,74 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{dbhijacker}
5
+ s.homepage = "https://github.com/crystalcommerce/hijacker"
6
+ s.version = "0.4.0"
7
+
8
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.authors = ["Michael Xavier", "Donald Plummer", "Woody Peterson"]
10
+ s.date = %q{2012-03-21}
11
+ s.description = %q{Allows a single Rails appliation to access many different databases}
12
+ s.email = %q{developers@crystalcommerce.com}
13
+ s.add_dependency("rails", "~>2.3.14")
14
+ s.add_development_dependency("rake", "~>0.9.2")
15
+ s.add_development_dependency("rack-test", "~>0.6.1")
16
+ s.add_development_dependency("rack", "~>1.1.0")
17
+ s.add_development_dependency("rspec", "~>2.8.0")
18
+ s.add_development_dependency("sqlite3", "~>1.3.5")
19
+ s.add_development_dependency("ruby-debug", "~>0.10.4")
20
+ s.extra_rdoc_files = [
21
+ "README.rdoc"
22
+ ]
23
+ s.files = %w{
24
+ Gemfile
25
+ Gemfile.lock
26
+ MIT-LICENSE
27
+ README.rdoc
28
+ Rakefile
29
+ VERSION
30
+ example_root_schema.rb
31
+ hijacker.gemspec
32
+ init.rb
33
+ install.rb
34
+ lib/dbhijacker.rb
35
+ lib/hijacker.rb
36
+ lib/hijacker/active_record_ext.rb
37
+ lib/hijacker/alias.rb
38
+ lib/hijacker/controller_methods.rb
39
+ lib/hijacker/database.rb
40
+ lib/hijacker/host.rb
41
+ lib/hijacker/middleware.rb
42
+ spec/hijacker/alias_spec.rb
43
+ spec/hijacker/database_spec.rb
44
+ spec/hijacker/host_spec.rb
45
+ spec/hijacker/middleware_spec.rb
46
+ spec/hijacker_spec.rb
47
+ spec/spec_helper.rb
48
+ tasks/hijacker_tasks.rake
49
+ uninstall.rb
50
+ }
51
+ s.rdoc_options = ["--charset=UTF-8"]
52
+ s.require_paths = ["lib"]
53
+ s.rubygems_version = %q{1.8.15}
54
+ s.summary = %q{One application, multiple client databases}
55
+ s.test_files = %w{
56
+ spec/hijacker/alias_spec.rb
57
+ spec/hijacker/database_spec.rb
58
+ spec/hijacker/host_spec.rb
59
+ spec/hijacker/middleware_spec.rb
60
+ spec/hijacker_spec.rb
61
+ spec/spec_helper.rb
62
+ }
63
+
64
+ if s.respond_to? :specification_version then
65
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
66
+ s.specification_version = 3
67
+
68
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
69
+ else
70
+ end
71
+ else
72
+ end
73
+ end
74
+
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require 'hijacker'
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
data/lib/dbhijacker.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/hijacker'
@@ -0,0 +1,8 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class MysqlAdapter
4
+ attr_accessor :config
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,7 @@
1
+ module Hijacker
2
+ class Alias < ActiveRecord::Base
3
+ establish_connection(Hijacker.root_config)
4
+
5
+ belongs_to :database, :class_name => "Hijacker::Database"
6
+ end
7
+ end
@@ -0,0 +1,46 @@
1
+ module Hijacker::ControllerMethods
2
+ module Instance
3
+ def hijack_connection
4
+ host = request.host
5
+
6
+ master, sister = determine_databases(host)
7
+
8
+ Hijacker.connect(master, sister)
9
+
10
+ return true
11
+ rescue Hijacker::InvalidDatabase => e
12
+ render_invalid_db
13
+
14
+ # If we've encountered a bad database connection, we don't want
15
+ # to continue rendering the rest of the before_filters on this, which it will
16
+ # try to do even when just rendering the bit of text above. If any filters
17
+ # return false, though, it will halt the filter chain.
18
+ return false
19
+ end
20
+
21
+ # Returns 2-member array of the main database to connect to, and the sister
22
+ # (sister will be nil if no master is found, which means we are on the master).
23
+ def determine_databases(host)
24
+ if Hijacker.do_hijacking?
25
+ Hijacker.config[:domain_patterns].find {|pattern| host =~ pattern}
26
+ client = $1
27
+ else # development, test, etc
28
+ client = ActiveRecord::Base.configurations[Rails.env]['database']
29
+ end
30
+
31
+ raise Hijacker::UnparseableURL, "cannot parse '#{host}'" if client.nil?
32
+
33
+ master, sister = Hijacker::Database.find_master_and_sister_for(client)
34
+
35
+ return [master, sister]
36
+ end
37
+
38
+ def render_invalid_db
39
+ render :text => "You do not appear to have an account with us (#{request.host})"
40
+ end
41
+ end
42
+ end
43
+
44
+ class ActionController::Base
45
+ include Hijacker::ControllerMethods::Instance
46
+ end
@@ -0,0 +1,113 @@
1
+ class Hijacker::Database < ActiveRecord::Base
2
+ establish_connection(Hijacker.root_config)
3
+
4
+ has_many :aliases, :class_name => "Hijacker::Alias"
5
+ belongs_to :master, :foreign_key => 'master_id', :class_name => 'Hijacker::Database'
6
+ has_many :sisters, :foreign_key => 'master_id', :class_name => 'Hijacker::Database'
7
+ belongs_to :host, :class_name => "Hijacker::Host"
8
+
9
+ validates_uniqueness_of :database
10
+
11
+ validates_presence_of :host_id
12
+
13
+ alias_attribute :name, :database
14
+
15
+ def self.find_by_name(name)
16
+ find_by_database(name)
17
+ end
18
+
19
+ def self.current
20
+ find(:first, :conditions => {:database => Hijacker.current_client})
21
+ end
22
+
23
+ # returns a string or nil
24
+ def self.find_master_for(client)
25
+ @masters ||= {}
26
+ @masters[client] ||= self.connection.select_values(
27
+ "SELECT master.database
28
+ FROM `databases` AS master, `databases` AS sister
29
+ WHERE sister.database = #{ActiveRecord::Base.connection.quote(client)}
30
+ AND sister.master_id = master.id"
31
+ ).first
32
+ end
33
+
34
+ # always returns a master, sister can be nil
35
+ def self.find_master_and_sister_for(client)
36
+ master = self.find_master_for(client)
37
+ sister = master.nil? ? nil : client
38
+ master ||= client
39
+
40
+ return master, sister
41
+ end
42
+
43
+ def self.shared_sites
44
+ self.find_shared_sites_for(Hijacker.current_client)
45
+ end
46
+
47
+ def self.connect_to_each_shared_site(&block)
48
+ connect_each(find_shared_sites_for(Hijacker.current_client), &block)
49
+ end
50
+
51
+ def self.connect_to_each_sister_site(&block)
52
+ sites = find_shared_sites_for(Hijacker.current_client)
53
+ sites.delete(Hijacker.current_client)
54
+ connect_each(sites, &block)
55
+ end
56
+
57
+ def self.find_shared_sites_for(client)
58
+ @shared_sites ||= {}
59
+ return @shared_sites[client] if @shared_sites[client].present?
60
+
61
+ current = self.find(:first, :conditions => {:database => client})
62
+ master_id = current.master_id || current.id
63
+
64
+ @shared_sites[client] = self.connection.select_values(
65
+ "SELECT `database`
66
+ FROM `databases`
67
+ WHERE master_id = '#{master_id}' OR id = '#{master_id}'
68
+ ORDER BY id"
69
+ )
70
+ end
71
+
72
+ def self.connect_each(sites = all.map(&:database))
73
+ original_database = Hijacker.current_client
74
+ begin
75
+ sites.each do |db|
76
+ Hijacker.connect_to_master(db)
77
+ yield db
78
+ end
79
+ ensure
80
+ begin
81
+ Hijacker.connect_to_master(original_database)
82
+ rescue Hijacker::InvalidDatabase
83
+ end
84
+ end
85
+ end
86
+
87
+ def self.count_each(&blk)
88
+ acc = {}
89
+ connect_each do |db|
90
+ count = blk.call
91
+ acc[db] = count if count > 0
92
+ end
93
+ width = acc.keys.map(&:length).max
94
+ acc.sort_by(&:last).each do |db, count|
95
+ puts("%#{width}s: %s" % [db, count])
96
+ end
97
+ acc
98
+ end
99
+
100
+ def self.disabled_databases
101
+ Hijacker::Database.connection.select_values("SELECT `database_name` FROM `disabled_databases`")
102
+ end
103
+
104
+ def disable!
105
+ Hijacker::Database.connection.
106
+ execute("REPLACE INTO `disabled_databases` (`database_name`) VALUES ('#{database}')")
107
+ end
108
+
109
+ def enable!
110
+ Hijacker::Database.connection.
111
+ execute("DELETE FROM `disabled_databases` WHERE `database_name` = '#{database}'")
112
+ end
113
+ end
@@ -0,0 +1,5 @@
1
+ class Hijacker::Host < ActiveRecord::Base
2
+ establish_connection(Hijacker.root_config)
3
+
4
+ validates_format_of :hostname, :with => /^(#{URI::REGEXP::PATTERN::HOST}|#{URI::REGEXP::PATTERN::IPV6ADDR})$/
5
+ end
@@ -0,0 +1,18 @@
1
+ module Hijacker
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ if env['HTTP_X_HIJACKER_DB'].present?
9
+ begin
10
+ Hijacker.connect(env['HTTP_X_HIJACKER_DB'])
11
+ rescue Hijacker::InvalidDatabase => e
12
+ return [404, {}, ""]
13
+ end
14
+ end
15
+ @app.call(env)
16
+ end
17
+ end
18
+ end
data/lib/hijacker.rb ADDED
@@ -0,0 +1,222 @@
1
+ require 'hijacker/active_record_ext'
2
+ require 'active_record'
3
+ require 'action_controller'
4
+
5
+ module Hijacker
6
+ class UnparseableURL < StandardError;end
7
+ class InvalidDatabase < StandardError;end
8
+
9
+ class << self
10
+ attr_accessor :config, :master, :sister
11
+ attr_writer :valid_routes
12
+ end
13
+
14
+ def self.valid_routes
15
+ @valid_routes ||= {}
16
+ end
17
+
18
+ def self.connect_to_master(db_name)
19
+ connect(*Hijacker::Database.find_master_and_sister_for(db_name))
20
+ end
21
+
22
+ # Manually establishes a new connection to the database.
23
+ #
24
+ # Background: every time rails gets information
25
+ # from the database, it uses the last established connection. So,
26
+ # although we've already established a connection to a "dummy" db
27
+ # ("crystal", in this case), if we establish a new connection, all
28
+ # subsequent database calls will use these settings instead (well,
29
+ # until it's called again when it gets another request).
30
+ #
31
+ # Note that you can manually call this from script/console (or wherever)
32
+ # to connect to the database you want, ex Hijacker.connect("database")
33
+ def self.connect(target_name, sister_name = nil, options = {})
34
+ raise InvalidDatabase, 'master cannot be nil' if target_name.nil?
35
+
36
+ target_name = target_name.downcase
37
+ sister_name = sister_name.downcase unless sister_name.nil?
38
+
39
+ return if already_connected?(target_name, sister_name) || test?
40
+
41
+ verify = options.fetch(:verify, Hijacker.do_hijacking?)
42
+
43
+ database = determine_database(target_name, sister_name, verify)
44
+
45
+ establish_connection_to_database(database)
46
+
47
+ check_connection
48
+
49
+ self.master = database.name
50
+ self.sister = sister_name
51
+
52
+ # don't cache sister site
53
+ cache_database_route(target_name, database) unless sister_name
54
+
55
+ connect_sister_site_models(target_name)
56
+
57
+ reenable_query_caching
58
+
59
+ self.config[:after_hijack].call if self.config[:after_hijack]
60
+ rescue
61
+ self.establish_root_connection
62
+ raise
63
+ end
64
+
65
+ # very small chance this will raise, but if it does, we will still handle it the
66
+ # same as +Hijacker.connect+ so we don't lock up the app.
67
+ #
68
+ # Also note that sister site models share a connection via minor management of
69
+ # AR's connection_pool stuff, and will use ActiveRecord::Base.connection_pool if
70
+ # we're not in a sister-site situation
71
+ def self.connect_sister_site_models(db)
72
+ return if db.nil?
73
+
74
+ sister_db_connection_pool = self.processing_sister_site? ? nil : ActiveRecord::Base.connection_pool
75
+ self.config[:sister_site_models].each do |model_name|
76
+ ar_model = model_name.constantize
77
+
78
+ if !sister_db_connection_pool
79
+ ar_model.establish_connection(self.root_connection.config.merge(:database => db))
80
+ begin
81
+ ar_model.connection
82
+ rescue
83
+ ar_model.establish_connection(self.root_connection.config)
84
+ raise Hijacker::InvalidDatabase, db
85
+ end
86
+ sister_db_connection_pool = ar_model.connection_pool
87
+ else
88
+ ActiveRecord::Base.connection_handler.connection_pools[model_name] = sister_db_connection_pool
89
+ end
90
+ end
91
+ end
92
+
93
+ # connects the sister_site_models to +db+ while calling the block
94
+ # if +db+ and self.master differ
95
+ def self.temporary_sister_connect(db, &block)
96
+ processing_sister_site = (db != self.master && db != self.sister)
97
+ self.sister = db if processing_sister_site
98
+ self.connect_sister_site_models(db) if processing_sister_site
99
+ result = block.call
100
+ self.connect_sister_site_models(self.master) if processing_sister_site
101
+ self.sister = nil if processing_sister_site
102
+ return result
103
+ end
104
+
105
+ # maintains and returns a connection to the "dummy" database.
106
+ #
107
+ # The advantage of using this over just calling
108
+ # ActiveRecord::Base.establish_connection (without arguments) to reconnect
109
+ # to the dummy database is that reusing the same connection greatly reduces
110
+ # context switching overhead etc involved with establishing a connection to
111
+ # the database. It may seem trivial, but it actually seems to speed things
112
+ # up by ~ 1/3 for already fast requests (probably less noticeable on slower
113
+ # pages).
114
+ #
115
+ # Note: does not hijack, just returns the root connection (i.e. AR::Base will
116
+ # maintain its connection)
117
+ def self.root_connection
118
+ unless $hijacker_root_connection
119
+ current_config = ActiveRecord::Base.connection.config
120
+ ActiveRecord::Base.establish_connection('root') # establish with defaults
121
+ $hijacker_root_connection = ActiveRecord::Base.connection
122
+ ActiveRecord::Base.establish_connection(current_config) # reconnect, we don't intend to hijack
123
+ end
124
+
125
+ return $hijacker_root_connection
126
+ end
127
+
128
+ def self.root_config
129
+ ActiveRecord::Base.configurations['root']
130
+ end
131
+
132
+ # this should establish a connection to a database containing the bare minimum
133
+ # for loading the app, usually a sessions table if using sql-based sessions.
134
+ def self.establish_root_connection
135
+ ActiveRecord::Base.establish_connection('root')
136
+ end
137
+
138
+ def self.processing_sister_site?
139
+ !sister.nil?
140
+ end
141
+
142
+ def self.master
143
+ @master || ActiveRecord::Base.configurations[ENV['RAILS_ENV'] || RAILS_ENV]['database']
144
+ end
145
+
146
+ def self.current_client
147
+ sister || master
148
+ end
149
+
150
+ def self.do_hijacking?
151
+ (Hijacker.config[:hosted_environments] || %w[staging production]).
152
+ include?(ENV['RAILS_ENV'])
153
+ end
154
+
155
+ # just calling establish_connection doesn't actually check to see if
156
+ # we've established a VALID connection. a call to connection will check
157
+ # this, and throw an error if the connection's invalid. It is important
158
+ # to catch the error and reconnect to a known valid database or rails
159
+ # will get stuck. This is because once we establish a connection to an
160
+ # invalid database, the next request will do a courteousy touch to the
161
+ # invalid database before reaching establish_connection and throw an error,
162
+ # preventing us from retrying to establish a valid connection and effectively
163
+ # locking us out of the app.
164
+ def self.check_connection
165
+ ::ActiveRecord::Base.connection
166
+ end
167
+
168
+ def self.test?
169
+ ['test', 'cucumber'].include?(RAILS_ENV)
170
+ end
171
+
172
+ private
173
+
174
+ def self.already_connected?(new_master, new_sister)
175
+ current_client == new_master && sister == new_sister
176
+ end
177
+
178
+ def self.determine_database(target_name, sister_name, verify)
179
+ if sister_name
180
+ database = Hijacker::Database.find_by_name(sister_name)
181
+ raise(Hijacker::InvalidDatabase, sister_name) if database.nil?
182
+ database
183
+ elsif valid_routes[target_name]
184
+ valid_routes[target_name] # cached valid database
185
+ else
186
+ database = Hijacker::Alias.find_by_name(target_name).try(:database) ||
187
+ Hijacker::Database.find_by_name(target_name)
188
+ raise(Hijacker::InvalidDatabase, target_name) if database.nil?
189
+ database
190
+ end
191
+ end
192
+
193
+ def self.cache_database_route(requested_db_name, actual_database)
194
+ valid_routes[requested_db_name] ||= actual_database
195
+ end
196
+
197
+ def self.establish_connection_to_database(database)
198
+ hijacked_config = self.root_connection.config.dup
199
+ ::ActiveRecord::Base.establish_connection(hijacked_config.merge(:database => database.name,
200
+ :host => database.host.hostname))
201
+ end
202
+
203
+ # This is a hack to get query caching back on. For some reason when we
204
+ # reconnect the database during the request, it stops doing query caching.
205
+ # We couldn't find how it's used by rails originally, but if you turn on
206
+ # query caching then start a cache block to initialize the @query_cache
207
+ # instance variable in the connection, AR will from then on build on that
208
+ # empty @query_cache hash. You have to do both 'cuz without the latter there
209
+ # will be no @query_cache available. Maybe someday we'll submit a ticket to Rails.
210
+ def self.reenable_query_caching
211
+ if ::ActionController::Base.perform_caching
212
+ ::ActiveRecord::Base.connection.instance_variable_set("@query_cache_enabled", true)
213
+ ::ActiveRecord::Base.connection.cache do;end
214
+ end
215
+ end
216
+ end
217
+
218
+ require 'hijacker/database'
219
+ require 'hijacker/alias'
220
+ require 'hijacker/host'
221
+ require 'hijacker/middleware'
222
+ require 'hijacker/controller_methods'
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hijacker::Alias do
4
+ it "belongs to a database" do
5
+ lambda { subject.database }.should_not raise_error
6
+ end
7
+ end
@@ -0,0 +1,76 @@
1
+ require "spec_helper"
2
+
3
+ module Hijacker
4
+ describe Database do
5
+ let(:host) { Hijacker::Host.create!(:hostname => "localhost") }
6
+ let(:alias_db) { Hijacker::Alias.new(:name => "alias_db") }
7
+
8
+ it "has many aliases" do
9
+ subject.aliases << alias_db
10
+ subject.aliases.should == [alias_db]
11
+ end
12
+
13
+ it "belongs to a host" do
14
+ subject.host = host
15
+ subject.host.should == host
16
+ end
17
+
18
+ it "requires a host" do
19
+ subject.host = nil
20
+ subject.should_not be_valid
21
+ subject.errors.on(:host_id).should == "can't be blank"
22
+
23
+ subject.host = host
24
+ subject.should be_valid
25
+ end
26
+
27
+ it "aliases name to database" do
28
+ subject.database = "foo"
29
+ subject.name.should == "foo"
30
+ subject.name = "bar"
31
+ subject.database.should == "bar"
32
+ end
33
+
34
+ it "aliases find_by_name to find_by_database" do
35
+ Hijacker::Database.should_receive(:find_by_database).with("foo")
36
+ Hijacker::Database.find_by_name("foo")
37
+ end
38
+
39
+ describe "#connect_each" do
40
+ def db(name)
41
+ mock("#{name}_db", :database => name)
42
+ end
43
+
44
+ before (:each) do
45
+ Database.stub!(:all).and_return([ db("one"), db("two"), db("three") ])
46
+ Hijacker.stub!(:connect)
47
+ end
48
+
49
+ it "Calls the block once for each database" do
50
+ count = 0
51
+ Database.connect_each do |db|
52
+ count += 1
53
+ end
54
+ count.should == Database.all.size
55
+ end
56
+
57
+ it "Passes the name of the database to the block" do
58
+ db_names = []
59
+ Database.connect_each do |db|
60
+ db_names << db
61
+ end
62
+ db_names.should == Database.all.map(&:database)
63
+ end
64
+
65
+ it "connects to each of the database and reconnects to the original" do
66
+ original_db = Hijacker::Database.current
67
+ Hijacker.should_receive(:connect).exactly(Database.all.size + 1).times
68
+ Database.connect_each do |db|
69
+ # noop
70
+ end
71
+
72
+ Hijacker::Database.current.should == original_db
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hijacker::Host do
4
+ it "validates the format of the hostname" do
5
+ subject.hostname = "lol nope"
6
+ subject.should_not be_valid
7
+ subject.errors.on(:hostname).should == "is invalid"
8
+
9
+ subject.hostname = nil
10
+ subject.should_not be_valid
11
+ subject.errors.on(:hostname).should == "is invalid"
12
+
13
+ subject.hostname = "db-01.example.com"
14
+ subject.should be_valid
15
+
16
+ subject.hostname = "192.168.1.1"
17
+ subject.should be_valid
18
+
19
+ subject.hostname = "2001:cdba::3257:9652"
20
+ subject.should be_valid
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+ require 'rack/test'
3
+
4
+ require 'hijacker/middleware'
5
+
6
+ module Hijacker
7
+ describe Middleware do
8
+ include Rack::Test::Methods
9
+
10
+ def app
11
+ Rack::Builder.new do
12
+ use Hijacker::Middleware
13
+ run lambda { |env| [200, { 'blah' => 'blah' }, "success"] }
14
+ end
15
+ end
16
+
17
+ describe "#call" do
18
+ context "When the 'X-Hijacker-DB' header is set" do
19
+ it "connects to the database from the header" do
20
+ Hijacker.should_receive(:connect).with("sample-db")
21
+ get '/',{}, 'HTTP_X_HIJACKER_DB' => 'sample-db'
22
+ end
23
+ end
24
+
25
+ context "When the 'X-Hijacker-DB' header is not set" do
26
+ it "doesn't connect to any database" do
27
+ Hijacker.should_not_receive(:connect)
28
+ get '/',{}, "x-not-db-header" => "something"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,181 @@
1
+ require "spec_helper"
2
+
3
+ describe Hijacker do
4
+ let(:hosted_environments) { %w[staging production] }
5
+
6
+ before(:each) do
7
+ Hijacker.config = {
8
+ :hosted_environments => hosted_environments
9
+ }
10
+ end
11
+
12
+ let!(:host) { Hijacker::Host.create!(:hostname => "localhost") }
13
+ let!(:master) { Hijacker::Database.create!(:database => "master_db", :host => host) }
14
+
15
+ describe ".find_shared_sites_for" do
16
+ let!(:sister) {Hijacker::Database.create(:database => "sister_db",
17
+ :master => master,
18
+ :host => host)}
19
+ let!(:sister2) {Hijacker::Database.create(:database => "sister_db2",
20
+ :master => master,
21
+ :host => host)}
22
+ let!(:unrelated) {Hijacker::Database.create(:database => "unrelated_db",
23
+ :host => host)}
24
+ let!(:unrelated_sister) {Hijacker::Database.create(:database => "unrelated_sister",
25
+ :master => unrelated,
26
+ :host => host)}
27
+
28
+ it "find shared sites given a master or sister database" do
29
+ dbs = ["master_db","sister_db","sister_db2"]
30
+ Hijacker::Database.find_shared_sites_for("master_db").should == dbs
31
+ Hijacker::Database.find_shared_sites_for("sister_db").should == dbs
32
+ end
33
+ end
34
+
35
+ describe "class methods" do
36
+ subject { Hijacker }
37
+
38
+ describe ".connect" do
39
+ let(:perform_caching) { false }
40
+
41
+ before(:each) do
42
+ subject.master = nil
43
+ subject.sister = nil
44
+ subject.valid_routes = {}
45
+ subject.stub(:test?).and_return(false)
46
+ ActiveRecord::Base.stub(:establish_connection)
47
+ subject.stub(:root_connection).and_return(stub(:config => {}))
48
+ subject.stub(:connect_sister_site_models)
49
+ Hijacker.stub(:do_hijacking?).and_return(true)
50
+ ::ActionController::Base.stub(:perform_caching).
51
+ and_return(perform_caching)
52
+ end
53
+
54
+ it "raises an InvalidDatabase exception if master is nil" do
55
+ expect { subject.connect(nil) }.to raise_error(Hijacker::InvalidDatabase)
56
+ end
57
+
58
+ it "establishes a connection merging in the db name and the hostname" do
59
+ Hijacker::Database.create!(:database => 'elsewhere', :host => host)
60
+ ActiveRecord::Base.should_receive(:establish_connection).
61
+ with({:database => 'elsewhere', :host => "localhost"})
62
+ subject.connect('elsewhere')
63
+ end
64
+
65
+ it "checks the connection by calling ActiveRecord::Base.connection" do
66
+ subject.should_receive(:check_connection)
67
+ subject.connect("master_db")
68
+ end
69
+
70
+ it "attempts to find an alias" do
71
+ Hijacker::Alias.should_receive(:find_by_name).with('alias_db')
72
+ subject.connect('alias_db') rescue nil
73
+ end
74
+
75
+ it "caches the valid route at the class level :(" do
76
+ subject.connect('master_db')
77
+ subject.valid_routes['master_db'].should == master
78
+ end
79
+
80
+ context "there's an alias for the master" do
81
+ let!(:alias_db) { Hijacker::Alias.create(:name => 'alias_db', :database => master)}
82
+
83
+ it "connects with the alias to the master and the host" do
84
+ ActiveRecord::Base.should_receive(:establish_connection).
85
+ with({:database => 'master_db', :host => "localhost"})
86
+ subject.connect('alias_db')
87
+ end
88
+
89
+ it "caches the valid route at the class level :(" do
90
+ subject.connect('alias_db')
91
+ subject.valid_routes['alias_db'].should == master
92
+ end
93
+ end
94
+
95
+ context "ActiveRecord reports the connection is invalid" do
96
+ before(:each) do
97
+ subject.stub(:check_connection).and_raise("oh no you didn't")
98
+ end
99
+
100
+ it "reestablishes the root connection" do
101
+ ActiveRecord::Base.should_receive(:establish_connection).with('root')
102
+ subject.connect('master_db') rescue nil
103
+ end
104
+
105
+ it "re-raises the error" do
106
+ expect { subject.connect("master_db") }.to raise_error("oh no you didn't")
107
+ end
108
+ end
109
+
110
+ context "already connected to database" do
111
+ before(:each) do
112
+ Hijacker.master = 'master_db'
113
+ Hijacker.sister = nil
114
+ end
115
+
116
+ after(:each) do
117
+ Hijacker.master = nil
118
+ end
119
+
120
+ it "does not reconnect" do
121
+ ActiveRecord::Base.should_not_receive(:establish_connection)
122
+ subject.connect('master_db')
123
+ end
124
+ end
125
+
126
+ context "sister site specified" do
127
+ let!(:sister_db) { Hijacker::Database.create!(:database => 'sister_db',
128
+ :master => master,
129
+ :host => host)}
130
+ it "does reconnect if specifying a different sister" do
131
+ ActiveRecord::Base.should_receive(:establish_connection)
132
+ subject.connect('master_db', 'sister_db')
133
+ end
134
+
135
+ it "does not cache the route" do
136
+ subject.connect('master_db', 'sister_db')
137
+ subject.valid_routes.should_not have_key('sister_db')
138
+ end
139
+
140
+ it "raises InvalidDatabase if the sister does not exist" do
141
+ expect do
142
+ subject.connect("master_db", "adopted_sister_db")
143
+ end.to raise_error(Hijacker::InvalidDatabase)
144
+ end
145
+ end
146
+
147
+ context "actioncontroller configured for caching" do
148
+ let(:perform_caching) { true }
149
+
150
+ it "enables the query cache on ActiveRecord::Base" do
151
+ subject.connect('master_db')
152
+ ::ActiveRecord::Base.connection.query_cache_enabled.should be_true
153
+ end
154
+
155
+ it "calls cache on the connection" do
156
+ ::ActiveRecord::Base.connection.should_receive(:cache)
157
+ subject.connect('master_db')
158
+ end
159
+ end
160
+
161
+ context "after_hijack call specified" do
162
+ let(:spy) { stub.as_null_object }
163
+ before(:each) do
164
+ Hijacker.config.merge!(:after_hijack => spy)
165
+ end
166
+
167
+ it "calls the callback" do
168
+ spy.should_receive(:call)
169
+ subject.connect('master_db')
170
+ end
171
+ end
172
+ end
173
+
174
+ describe ".check_connection" do
175
+ it "calls connection on ActiveRecord::Base" do
176
+ ::ActiveRecord::Base.should_receive(:connection)
177
+ subject.check_connection
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'active_support/test_case'
4
+ require 'ruby-debug'
5
+
6
+ require 'active_record'
7
+
8
+ RAILS_ENV="test"
9
+ ENV['RAILS_ENV'] = 'test'
10
+ $:.unshift '../lib'
11
+ ActiveRecord::Base.configurations = {
12
+ "test" => {
13
+ :adapter => 'sqlite3',
14
+ :database => File.dirname(__FILE__) + "/test_database.sqlite3"
15
+ }
16
+ }
17
+
18
+ ActiveRecord::Base.establish_connection
19
+ require File.dirname(__FILE__) + "/../example_root_schema"
20
+
21
+ require 'hijacker'
22
+
23
+ RSpec.configure do |config|
24
+ config.before(:each) do
25
+ Hijacker::Database.delete_all
26
+ Hijacker::Alias.delete_all
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :hijacker do
3
+ # # Task goes here
4
+ # end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,209 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dbhijacker
3
+ version: !ruby/object:Gem::Version
4
+ hash: 15
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
11
+ platform: ruby
12
+ authors:
13
+ - Michael Xavier
14
+ - Donald Plummer
15
+ - Woody Peterson
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2012-03-21 00:00:00 Z
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rails
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 31
31
+ segments:
32
+ - 2
33
+ - 3
34
+ - 14
35
+ version: 2.3.14
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: rake
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ hash: 63
47
+ segments:
48
+ - 0
49
+ - 9
50
+ - 2
51
+ version: 0.9.2
52
+ type: :development
53
+ version_requirements: *id002
54
+ - !ruby/object:Gem::Dependency
55
+ name: rack-test
56
+ prerelease: false
57
+ requirement: &id003 !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ hash: 5
63
+ segments:
64
+ - 0
65
+ - 6
66
+ - 1
67
+ version: 0.6.1
68
+ type: :development
69
+ version_requirements: *id003
70
+ - !ruby/object:Gem::Dependency
71
+ name: rack
72
+ prerelease: false
73
+ requirement: &id004 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ hash: 19
79
+ segments:
80
+ - 1
81
+ - 1
82
+ - 0
83
+ version: 1.1.0
84
+ type: :development
85
+ version_requirements: *id004
86
+ - !ruby/object:Gem::Dependency
87
+ name: rspec
88
+ prerelease: false
89
+ requirement: &id005 !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ hash: 47
95
+ segments:
96
+ - 2
97
+ - 8
98
+ - 0
99
+ version: 2.8.0
100
+ type: :development
101
+ version_requirements: *id005
102
+ - !ruby/object:Gem::Dependency
103
+ name: sqlite3
104
+ prerelease: false
105
+ requirement: &id006 !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ hash: 17
111
+ segments:
112
+ - 1
113
+ - 3
114
+ - 5
115
+ version: 1.3.5
116
+ type: :development
117
+ version_requirements: *id006
118
+ - !ruby/object:Gem::Dependency
119
+ name: ruby-debug
120
+ prerelease: false
121
+ requirement: &id007 !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ~>
125
+ - !ruby/object:Gem::Version
126
+ hash: 63
127
+ segments:
128
+ - 0
129
+ - 10
130
+ - 4
131
+ version: 0.10.4
132
+ type: :development
133
+ version_requirements: *id007
134
+ description: Allows a single Rails appliation to access many different databases
135
+ email: developers@crystalcommerce.com
136
+ executables: []
137
+
138
+ extensions: []
139
+
140
+ extra_rdoc_files:
141
+ - README.rdoc
142
+ files:
143
+ - Gemfile
144
+ - Gemfile.lock
145
+ - MIT-LICENSE
146
+ - README.rdoc
147
+ - Rakefile
148
+ - VERSION
149
+ - example_root_schema.rb
150
+ - hijacker.gemspec
151
+ - init.rb
152
+ - install.rb
153
+ - lib/dbhijacker.rb
154
+ - lib/hijacker.rb
155
+ - lib/hijacker/active_record_ext.rb
156
+ - lib/hijacker/alias.rb
157
+ - lib/hijacker/controller_methods.rb
158
+ - lib/hijacker/database.rb
159
+ - lib/hijacker/host.rb
160
+ - lib/hijacker/middleware.rb
161
+ - spec/hijacker/alias_spec.rb
162
+ - spec/hijacker/database_spec.rb
163
+ - spec/hijacker/host_spec.rb
164
+ - spec/hijacker/middleware_spec.rb
165
+ - spec/hijacker_spec.rb
166
+ - spec/spec_helper.rb
167
+ - tasks/hijacker_tasks.rake
168
+ - uninstall.rb
169
+ homepage: https://github.com/crystalcommerce/hijacker
170
+ licenses: []
171
+
172
+ post_install_message:
173
+ rdoc_options:
174
+ - --charset=UTF-8
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ hash: 3
183
+ segments:
184
+ - 0
185
+ version: "0"
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ none: false
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ hash: 3
192
+ segments:
193
+ - 0
194
+ version: "0"
195
+ requirements: []
196
+
197
+ rubyforge_project:
198
+ rubygems_version: 1.8.15
199
+ signing_key:
200
+ specification_version: 3
201
+ summary: One application, multiple client databases
202
+ test_files:
203
+ - spec/hijacker/alias_spec.rb
204
+ - spec/hijacker/database_spec.rb
205
+ - spec/hijacker/host_spec.rb
206
+ - spec/hijacker/middleware_spec.rb
207
+ - spec/hijacker_spec.rb
208
+ - spec/spec_helper.rb
209
+ has_rdoc: