db-hijacker 0.3.1

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
+
@@ -0,0 +1,61 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ db-hijacker (0.3.1)
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
+ db-hijacker!
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)
@@ -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.
@@ -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
@@ -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,16 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table "databases", :force => true do |t|
3
+ t.string "database"
4
+ t.integer "master_id"
5
+ end
6
+
7
+ add_index "databases", "database"
8
+ add_index "databases", "master_id"
9
+
10
+ create_table "aliases", :force => true do |t|
11
+ t.integer "database_id"
12
+ t.string "name"
13
+ end
14
+
15
+ add_index "aliases", "name"
16
+ end
@@ -0,0 +1,70 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{db-hijacker}
5
+ s.homepage = "https://github.com/crystalcommerce/hijacker"
6
+ s.version = "0.3.1"
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/hijacker.rb
35
+ lib/hijacker/active_record_ext.rb
36
+ lib/hijacker/alias.rb
37
+ lib/hijacker/controller_methods.rb
38
+ lib/hijacker/database.rb
39
+ lib/hijacker/middleware.rb
40
+ spec/hijacker/alias_spec.rb
41
+ spec/hijacker/database_spec.rb
42
+ spec/hijacker/middleware_spec.rb
43
+ spec/hijacker_spec.rb
44
+ spec/spec_helper.rb
45
+ tasks/hijacker_tasks.rake
46
+ uninstall.rb
47
+ }
48
+ s.rdoc_options = ["--charset=UTF-8"]
49
+ s.require_paths = ["lib"]
50
+ s.rubygems_version = %q{1.8.15}
51
+ s.summary = %q{One application, multiple client databases}
52
+ s.test_files = %w{
53
+ spec/hijacker/alias_spec.rb
54
+ spec/hijacker/database_spec.rb
55
+ spec/hijacker/middleware_spec.rb
56
+ spec/hijacker_spec.rb
57
+ spec/spec_helper.rb
58
+ }
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ else
66
+ end
67
+ else
68
+ end
69
+ end
70
+
data/init.rb ADDED
@@ -0,0 +1,7 @@
1
+ # Include hook code here
2
+ require 'hijacker'
3
+ require 'hijacker/controller_methods'
4
+
5
+ class ActionController::Base
6
+ include Hijacker::ControllerMethods::Instance
7
+ end
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,218 @@
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
+ db_name = determine_database_name(target_name, sister_name, verify)
44
+
45
+ establish_connection_to_database(db_name)
46
+
47
+ check_connection
48
+
49
+ self.master = db_name
50
+ self.sister = sister_name
51
+
52
+ # don't cache sister site
53
+ cache_database_route(target_name, db_name) 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']]['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_name(target_name, sister_name, verify)
179
+ if sister_name
180
+ raise(Hijacker::InvalidDatabase, sister_name) unless Hijacker::Database.exists?(:database => sister_name)
181
+ db_name = sister_name
182
+ elsif valid_routes[target_name]
183
+ db_name = valid_routes[target_name] # cached valid name
184
+ else
185
+ db_name = target_name unless verify
186
+ db_name ||= Hijacker::Alias.find_by_name(target_name).try(:database).try(:database)
187
+ db_name ||= Hijacker::Database.find_by_database(target_name).try(:database)
188
+ raise(Hijacker::InvalidDatabase, target_name) if db_name.nil?
189
+ end
190
+ db_name
191
+ end
192
+
193
+ def self.cache_database_route(requested_db_name, actual_db_name)
194
+ valid_routes[requested_db_name] ||= actual_db_name
195
+ end
196
+
197
+ def self.establish_connection_to_database(db_name)
198
+ hijacked_config = self.root_connection.config.dup
199
+ ::ActiveRecord::Base.establish_connection(hijacked_config.merge(:database => db_name))
200
+ end
201
+
202
+ # This is a hack to get query caching back on. For some reason when we
203
+ # reconnect the database during the request, it stops doing query caching.
204
+ # We couldn't find how it's used by rails originally, but if you turn on
205
+ # query caching then start a cache block to initialize the @query_cache
206
+ # instance variable in the connection, AR will from then on build on that
207
+ # empty @query_cache hash. You have to do both 'cuz without the latter there
208
+ # will be no @query_cache available. Maybe someday we'll submit a ticket to Rails.
209
+ def self.reenable_query_caching
210
+ if ::ActionController::Base.perform_caching
211
+ ::ActiveRecord::Base.connection.instance_variable_set("@query_cache_enabled", true)
212
+ ::ActiveRecord::Base.connection.cache do;end
213
+ end
214
+ end
215
+ end
216
+
217
+ require 'hijacker/database'
218
+ require 'hijacker/alias'
@@ -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,42 @@
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
@@ -0,0 +1,91 @@
1
+ class Hijacker::Database < ActiveRecord::Base
2
+ establish_connection(Hijacker.root_config)
3
+
4
+ validates_uniqueness_of :database
5
+
6
+ has_many :aliases, :class_name => "Hijacker::Alias"
7
+ belongs_to :master, :foreign_key => 'master_id', :class_name => 'Hijacker::Database'
8
+ has_many :sisters, :foreign_key => 'master_id', :class_name => 'Hijacker::Database'
9
+
10
+ def self.current
11
+ find(:first, :conditions => {:database => Hijacker.current_client})
12
+ end
13
+
14
+ # returns a string or nil
15
+ def self.find_master_for(client)
16
+ @masters ||= {}
17
+ @masters[client] ||= self.connection.select_values(
18
+ "SELECT master.database
19
+ FROM `databases` AS master, `databases` AS sister
20
+ WHERE sister.database = #{ActiveRecord::Base.connection.quote(client)}
21
+ AND sister.master_id = master.id"
22
+ ).first
23
+ end
24
+
25
+ # always returns a master, sister can be nil
26
+ def self.find_master_and_sister_for(client)
27
+ master = self.find_master_for(client)
28
+ sister = master.nil? ? nil : client
29
+ master ||= client
30
+
31
+ return master, sister
32
+ end
33
+
34
+ def self.shared_sites
35
+ self.find_shared_sites_for(Hijacker.current_client)
36
+ end
37
+
38
+ def self.connect_to_each_shared_site(&block)
39
+ connect_each(find_shared_sites_for(Hijacker.current_client), &block)
40
+ end
41
+
42
+ def self.connect_to_each_sister_site(&block)
43
+ sites = find_shared_sites_for(Hijacker.current_client)
44
+ sites.delete(Hijacker.current_client)
45
+ connect_each(sites, &block)
46
+ end
47
+
48
+ def self.find_shared_sites_for(client)
49
+ @shared_sites ||= {}
50
+ return @shared_sites[client] if @shared_sites[client].present?
51
+
52
+ current = self.find(:first, :conditions => {:database => client})
53
+ master_id = current.master_id || current.id
54
+
55
+ @shared_sites[client] = self.connection.select_values(
56
+ "SELECT `database`
57
+ FROM `databases`
58
+ WHERE master_id = '#{master_id}' OR id = '#{master_id}'
59
+ ORDER BY id"
60
+ )
61
+ end
62
+
63
+ def self.connect_each(sites = all.map(&:database))
64
+ original_database = Hijacker.current_client
65
+ begin
66
+ sites.each do |db|
67
+ Hijacker.connect_to_master(db)
68
+ yield db
69
+ end
70
+ ensure
71
+ begin
72
+ Hijacker.connect_to_master(original_database)
73
+ rescue Hijacker::InvalidDatabase
74
+ end
75
+ end
76
+ end
77
+
78
+ def self.disabled_databases
79
+ Hijacker::Database.connection.select_values("SELECT `database_name` FROM `disabled_databases`")
80
+ end
81
+
82
+ def disable!
83
+ Hijacker::Database.connection.
84
+ execute("REPLACE INTO `disabled_databases` (`database_name`) VALUES ('#{database}')")
85
+ end
86
+
87
+ def enable!
88
+ Hijacker::Database.connection.
89
+ execute("DELETE FROM `disabled_databases` WHERE `database_name` = '#{database}'")
90
+ end
91
+ 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
@@ -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,46 @@
1
+ require "spec_helper"
2
+
3
+ module Hijacker
4
+ describe Database do
5
+ it "has many aliases" do
6
+ lambda { subject.aliases }.should_not raise_error
7
+ end
8
+
9
+ describe "#connect_each" do
10
+ def db(name)
11
+ mock("#{name}_db", :database => name)
12
+ end
13
+
14
+ before (:each) do
15
+ Database.stub!(:all).and_return([ db("one"), db("two"), db("three") ])
16
+ Hijacker.stub!(:connect)
17
+ end
18
+
19
+ it "Calls the block once for each database" do
20
+ count = 0
21
+ Database.connect_each do |db|
22
+ count += 1
23
+ end
24
+ count.should == Database.all.size
25
+ end
26
+
27
+ it "Passes the name of the database to the block" do
28
+ db_names = []
29
+ Database.connect_each do |db|
30
+ db_names << db
31
+ end
32
+ db_names.should == Database.all.map(&:database)
33
+ end
34
+
35
+ it "connects to each of the database and reconnects to the original" do
36
+ original_db = Hijacker::Database.current
37
+ Hijacker.should_receive(:connect).exactly(Database.all.size + 1).times
38
+ Database.connect_each do |db|
39
+ # noop
40
+ end
41
+
42
+ Hijacker::Database.current.should == original_db
43
+ end
44
+ end
45
+ end
46
+ 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,176 @@
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!(:master) { Hijacker::Database.create(:database => "master_db") }
13
+
14
+ describe ".find_shared_sites_for" do
15
+ let!(:sister) {Hijacker::Database.create(:database => "sister_db",
16
+ :master => master)}
17
+ let!(:sister2) {Hijacker::Database.create(:database => "sister_db2",
18
+ :master => master)}
19
+ let!(:unrelated) {Hijacker::Database.create(:database => "unrelated_db")}
20
+ let!(:unrelated_sister) {Hijacker::Database.create(:database => "unrelated_sister",
21
+ :master => unrelated)}
22
+
23
+ it "find shared sites given a master or sister database" do
24
+ dbs = ["master_db","sister_db","sister_db2"]
25
+ Hijacker::Database.find_shared_sites_for("master_db").should == dbs
26
+ Hijacker::Database.find_shared_sites_for("sister_db").should == dbs
27
+ end
28
+ end
29
+
30
+ describe "class methods" do
31
+ subject { Hijacker }
32
+
33
+ describe ".connect" do
34
+ let(:perform_caching) { false }
35
+
36
+
37
+ before(:each) do
38
+ subject.master = nil
39
+ subject.sister = nil
40
+ subject.valid_routes = {}
41
+ subject.stub(:test?).and_return(false)
42
+ ActiveRecord::Base.stub(:establish_connection)
43
+ subject.stub(:root_connection).and_return(stub(:config => {}))
44
+ subject.stub(:connect_sister_site_models)
45
+ Hijacker.stub(:do_hijacking?).and_return(true)
46
+ ::ActionController::Base.stub(:perform_caching).
47
+ and_return(perform_caching)
48
+ end
49
+
50
+ it "raises an InvalidDatabase exception if master is nil" do
51
+ expect { subject.connect(nil) }.to raise_error(Hijacker::InvalidDatabase)
52
+ end
53
+
54
+ it "establishes a connection merging in the db name" do
55
+ Hijacker::Database.create!(:database => 'elsewhere')
56
+ ActiveRecord::Base.should_receive(:establish_connection).
57
+ with({:database => 'elsewhere'})
58
+ subject.connect('elsewhere')
59
+ end
60
+
61
+ it "checks the connection by calling ActiveRecord::Base.connection" do
62
+ subject.should_receive(:check_connection)
63
+ subject.connect("master_db")
64
+ end
65
+
66
+ it "attempts to find an alias" do
67
+ Hijacker::Alias.should_receive(:find_by_name).with('alias_db')
68
+ subject.connect('alias_db') rescue nil
69
+ end
70
+
71
+ it "caches the valid route at the class level :(" do
72
+ subject.connect('master_db')
73
+ subject.valid_routes['master_db'].should == 'master_db'
74
+ end
75
+
76
+ context "there's an alias for the master" do
77
+ let!(:alias_db) { Hijacker::Alias.create(:name => 'alias_db', :database => master)}
78
+
79
+ it "connects with the alias" do
80
+ ActiveRecord::Base.should_receive(:establish_connection).
81
+ with({:database => 'master_db'})
82
+ subject.connect('alias_db')
83
+ end
84
+
85
+ it "caches the valid route at the class level :(" do
86
+ subject.connect('alias_db')
87
+ subject.valid_routes['alias_db'].should == 'master_db'
88
+ end
89
+ end
90
+
91
+ context "ActiveRecord reports the connection is invalid" do
92
+ before(:each) do
93
+ subject.stub(:check_connection).and_raise("oh no you didn't")
94
+ end
95
+
96
+ it "reestablishes the root connection" do
97
+ ActiveRecord::Base.should_receive(:establish_connection).with('root')
98
+ subject.connect('master_db') rescue nil
99
+ end
100
+
101
+ it "re-raises the error" do
102
+ expect { subject.connect("master_db") }.to raise_error("oh no you didn't")
103
+ end
104
+ end
105
+
106
+ context "already connected to database" do
107
+ before(:each) do
108
+ Hijacker.master = 'master_db'
109
+ Hijacker.sister = nil
110
+ end
111
+
112
+ after(:each) do
113
+ Hijacker.master = nil
114
+ end
115
+
116
+ it "does not reconnect" do
117
+ ActiveRecord::Base.should_not_receive(:establish_connection)
118
+ subject.connect('master_db')
119
+ end
120
+ end
121
+
122
+ context "sister site specified" do
123
+ let!(:sister_db) { Hijacker::Database.create!(:database => 'sister_db',
124
+ :master => master)}
125
+ it "does reconnect if specifying a different sister" do
126
+ ActiveRecord::Base.should_receive(:establish_connection)
127
+ subject.connect('master_db', 'sister_db')
128
+ end
129
+
130
+ it "does not cache the route" do
131
+ subject.connect('master_db', 'sister_db')
132
+ subject.valid_routes.should_not have_key('sister_db')
133
+ end
134
+
135
+ it "raises InvalidDatabase if the sister does not exist" do
136
+ expect do
137
+ subject.connect("master_db", "adopted_sister_db")
138
+ end.to raise_error(Hijacker::InvalidDatabase)
139
+ end
140
+ end
141
+
142
+ context "actioncontroller configured for caching" do
143
+ let(:perform_caching) { true }
144
+
145
+ it "enables the query cache on ActiveRecord::Base" do
146
+ subject.connect('master_db')
147
+ ::ActiveRecord::Base.connection.query_cache_enabled.should be_true
148
+ end
149
+
150
+ it "calls cache on the connection" do
151
+ ::ActiveRecord::Base.connection.should_receive(:cache)
152
+ subject.connect('master_db')
153
+ end
154
+ end
155
+
156
+ context "after_hijack call specified" do
157
+ let(:spy) { stub.as_null_object }
158
+ before(:each) do
159
+ Hijacker.config.merge!(:after_hijack => spy)
160
+ end
161
+
162
+ it "calls the callback" do
163
+ spy.should_receive(:call)
164
+ subject.connect('master_db')
165
+ end
166
+ end
167
+ end
168
+
169
+ describe ".check_connection" do
170
+ it "calls connection on ActiveRecord::Base" do
171
+ ::ActiveRecord::Base.should_receive(:connection)
172
+ subject.check_connection
173
+ end
174
+ end
175
+ end
176
+ 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
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,205 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db-hijacker
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 1
10
+ version: 0.3.1
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/hijacker.rb
154
+ - lib/hijacker/active_record_ext.rb
155
+ - lib/hijacker/alias.rb
156
+ - lib/hijacker/controller_methods.rb
157
+ - lib/hijacker/database.rb
158
+ - lib/hijacker/middleware.rb
159
+ - spec/hijacker/alias_spec.rb
160
+ - spec/hijacker/database_spec.rb
161
+ - spec/hijacker/middleware_spec.rb
162
+ - spec/hijacker_spec.rb
163
+ - spec/spec_helper.rb
164
+ - tasks/hijacker_tasks.rake
165
+ - uninstall.rb
166
+ homepage: https://github.com/crystalcommerce/hijacker
167
+ licenses: []
168
+
169
+ post_install_message:
170
+ rdoc_options:
171
+ - --charset=UTF-8
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ hash: 3
180
+ segments:
181
+ - 0
182
+ version: "0"
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ hash: 3
189
+ segments:
190
+ - 0
191
+ version: "0"
192
+ requirements: []
193
+
194
+ rubyforge_project:
195
+ rubygems_version: 1.8.15
196
+ signing_key:
197
+ specification_version: 3
198
+ summary: One application, multiple client databases
199
+ test_files:
200
+ - spec/hijacker/alias_spec.rb
201
+ - spec/hijacker/database_spec.rb
202
+ - spec/hijacker/middleware_spec.rb
203
+ - spec/hijacker_spec.rb
204
+ - spec/spec_helper.rb
205
+ has_rdoc: