db-hijacker 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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: