apartment 0.15.0 → 0.16.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.
Files changed (35) hide show
  1. data/.pryrc +3 -0
  2. data/.travis.yml +5 -0
  3. data/HISTORY.md +8 -0
  4. data/README.md +66 -14
  5. data/Rakefile +11 -10
  6. data/apartment.gemspec +6 -5
  7. data/lib/apartment.rb +21 -3
  8. data/lib/apartment/adapters/{mysql_adapter.rb → mysql2_adapter.rb} +7 -7
  9. data/lib/apartment/adapters/postgresql_adapter.rb +26 -15
  10. data/lib/apartment/database.rb +3 -0
  11. data/lib/apartment/elevators/domain.rb +18 -0
  12. data/lib/apartment/elevators/generic.rb +27 -0
  13. data/lib/apartment/elevators/subdomain.rb +5 -19
  14. data/lib/apartment/version.rb +1 -1
  15. data/spec/adapters/{mysql_adapter_spec.rb → mysql2_adapter_spec.rb} +7 -7
  16. data/spec/adapters/postgresql_adapter_spec.rb +4 -4
  17. data/spec/config/database.yml +2 -1
  18. data/spec/database_spec.rb +49 -26
  19. data/spec/dummy/config/application.rb +3 -0
  20. data/spec/examples/db_adapter_examples.rb +8 -8
  21. data/spec/examples/elevator_examples.rb +31 -0
  22. data/spec/examples/generic_adapter_examples.rb +10 -10
  23. data/spec/examples/schema_adapter_examples.rb +125 -38
  24. data/spec/integration/delayed_job_integration_spec.rb +1 -1
  25. data/spec/integration/middleware/domain_elevator_spec.rb +9 -0
  26. data/spec/integration/middleware/generic_elevator_spec.rb +10 -0
  27. data/spec/integration/middleware/subdomain_elevator_spec.rb +6 -49
  28. data/spec/spec_helper.rb +5 -2
  29. data/spec/support/apartment_helpers.rb +13 -10
  30. data/spec/support/contexts.rb +54 -0
  31. data/spec/support/requirements.rb +4 -4
  32. data/spec/unit/config_spec.rb +0 -4
  33. data/spec/unit/middleware/domain_elevator_spec.rb +26 -0
  34. data/spec/unit/middleware/subdomain_elevator_spec.rb +7 -7
  35. metadata +119 -108
@@ -0,0 +1,27 @@
1
+ module Apartment
2
+ module Elevators
3
+ # Provides a rack based db switching solution based on request
4
+ #
5
+ class Generic
6
+
7
+ def initialize(app, processor = nil)
8
+ @app = app
9
+ @processor = processor || method(:parse_database_name)
10
+ end
11
+
12
+ def call(env)
13
+ request = ActionDispatch::Request.new(env)
14
+
15
+ database = @processor.call(request)
16
+
17
+ Apartment::Database.switch database if database
18
+
19
+ @app.call(env)
20
+ end
21
+
22
+ def parse_database_name(request)
23
+ raise "Override"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,27 +1,13 @@
1
1
  module Apartment
2
2
  module Elevators
3
- # Provides a rack based db switching solution based on subdomains
4
- # Assumes that database name should match subdomain
5
- class Subdomain
3
+ # Provides a rack based db switching solution based on subdomains
4
+ # Assumes that database name should match subdomain
5
+ #
6
+ class Subdomain < Generic
6
7
 
7
- def initialize(app)
8
- @app = app
9
- end
10
-
11
- def call(env)
12
- request = ActionDispatch::Request.new(env)
13
-
14
- database = subdomain(request)
15
-
16
- Apartment::Database.switch database if database
17
-
18
- @app.call(env)
19
- end
20
-
21
- def subdomain(request)
8
+ def parse_database_name(request)
22
9
  request.subdomain.present? && request.subdomain || nil
23
10
  end
24
-
25
11
  end
26
12
  end
27
13
  end
@@ -1,3 +1,3 @@
1
1
  module Apartment
2
- VERSION = "0.15.0"
2
+ VERSION = "0.16.0"
3
3
  end
@@ -1,17 +1,17 @@
1
1
  require 'spec_helper'
2
- require 'apartment/adapters/mysql_adapter'
2
+ require 'apartment/adapters/mysql2_adapter'
3
+
4
+ describe Apartment::Adapters::Mysql2Adapter do
3
5
 
4
- describe Apartment::Adapters::MysqlAdapter do
5
-
6
6
  let(:config){ Apartment::Test.config['connections']['mysql'] }
7
7
  subject{ Apartment::Database.mysql2_adapter config.symbolize_keys }
8
-
8
+
9
9
  def database_names
10
10
  ActiveRecord::Base.connection.execute("SELECT schema_name FROM information_schema.schemata").collect{|row| row[0]}
11
11
  end
12
-
12
+
13
13
  let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
14
-
14
+
15
15
  it_should_behave_like "a generic apartment adapter"
16
16
  it_should_behave_like "a db based apartment adapter"
17
- end
17
+ end
@@ -14,22 +14,22 @@ describe Apartment::Adapters::PostgresqlAdapter do
14
14
  def database_names
15
15
  ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect{|row| row['nspname']}
16
16
  end
17
-
17
+
18
18
  let(:default_database){ subject.process{ ActiveRecord::Base.connection.schema_search_path } }
19
19
 
20
20
  it_should_behave_like "a generic apartment adapter"
21
21
  it_should_behave_like "a schema based apartment adapter"
22
22
  end
23
-
23
+
24
24
  context "using databases" do
25
25
 
26
- before{ Apartment.use_postgres_schemas = false }
26
+ before{ Apartment.use_postgres_schemas = false }
27
27
 
28
28
  # Not sure why, but somehow using let(:database_names) memoizes for the whole example group, not just each test
29
29
  def database_names
30
30
  connection.execute("select datname from pg_database;").collect{|row| row['datname']}
31
31
  end
32
-
32
+
33
33
  let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
34
34
 
35
35
  it_should_behave_like "a generic apartment adapter"
@@ -3,7 +3,8 @@ connections:
3
3
  adapter: postgresql
4
4
  database: apartment_postgresql_test
5
5
  min_messages: WARNING
6
- username: root
6
+ username: postgres
7
+ schema_search_path: public
7
8
  password:
8
9
 
9
10
  mysql:
@@ -1,44 +1,67 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Apartment::Database do
4
-
4
+ context "using mysql" do
5
+ # See apartment.yml file in dummy app config
6
+
7
+ let(:config){ Apartment::Test.config['connections']['mysql'].symbolize_keys }
8
+
9
+ before do
10
+ ActiveRecord::Base.establish_connection config
11
+ Apartment::Test.load_schema # load the Rails schema in the public db schema
12
+ subject.stub(:config).and_return config # Use postgresql database config for this test
13
+ end
14
+
15
+ describe "#adapter" do
16
+ before do
17
+ subject.reload!
18
+ end
19
+
20
+ it "should load mysql adapter" do
21
+ subject.adapter
22
+ Apartment::Adapters::Mysql2Adapter.should be_a(Class)
23
+ end
24
+
25
+ end
26
+ end
27
+
5
28
  context "using postgresql" do
6
-
29
+
7
30
  # See apartment.yml file in dummy app config
8
-
31
+
9
32
  let(:config){ Apartment::Test.config['connections']['postgresql'].symbolize_keys }
10
33
  let(:database){ Apartment::Test.next_db }
11
34
  let(:database2){ Apartment::Test.next_db }
12
-
35
+
13
36
  before do
14
37
  Apartment.use_postgres_schemas = true
15
38
  ActiveRecord::Base.establish_connection config
16
39
  Apartment::Test.load_schema # load the Rails schema in the public db schema
17
40
  subject.stub(:config).and_return config # Use postgresql database config for this test
18
41
  end
19
-
42
+
20
43
  describe "#adapter" do
21
44
  before do
22
45
  subject.reload!
23
46
  end
24
-
47
+
25
48
  it "should load postgresql adapter" do
26
49
  subject.adapter
27
50
  Apartment::Adapters::PostgresqlAdapter.should be_a(Class)
28
51
  end
29
-
52
+
30
53
  it "should raise exception with invalid adapter specified" do
31
54
  subject.stub(:config).and_return config.merge(:adapter => 'unkown')
32
-
55
+
33
56
  expect {
34
57
  Apartment::Database.adapter
35
58
  }.to raise_error
36
59
  end
37
-
60
+
38
61
  end
39
-
62
+
40
63
  context "with schemas" do
41
-
64
+
42
65
  before do
43
66
  Apartment.configure do |config|
44
67
  config.excluded_models = []
@@ -47,25 +70,25 @@ describe Apartment::Database do
47
70
  end
48
71
  subject.create database
49
72
  end
50
-
73
+
51
74
  after{ subject.drop database }
52
-
75
+
53
76
  describe "#create" do
54
77
  it "should seed data" do
55
78
  subject.switch database
56
79
  User.count.should be > 0
57
80
  end
58
81
  end
59
-
82
+
60
83
  describe "#switch" do
61
-
84
+
62
85
  let(:x){ rand(3) }
63
-
86
+
64
87
  context "creating models" do
65
-
88
+
66
89
  before{ subject.create database2 }
67
90
  after{ subject.drop database2 }
68
-
91
+
69
92
  it "should create a model instance in the current schema" do
70
93
  subject.switch database2
71
94
  db2_count = User.count + x.times{ User.create }
@@ -80,20 +103,20 @@ describe Apartment::Database do
80
103
  User.count.should == db_count
81
104
  end
82
105
  end
83
-
106
+
84
107
  context "with excluded models" do
85
-
108
+
86
109
  before do
87
110
  Apartment.configure do |config|
88
111
  config.excluded_models = ["Company"]
89
112
  end
90
113
  subject.init
91
114
  end
92
-
115
+
93
116
  it "should create excluded models in public schema" do
94
117
  subject.reset # ensure we're on public schema
95
118
  count = Company.count + x.times{ Company.create }
96
-
119
+
97
120
  subject.switch database
98
121
  x.times{ Company.create }
99
122
  Company.count.should == count + x
@@ -101,10 +124,10 @@ describe Apartment::Database do
101
124
  Company.count.should == count + x
102
125
  end
103
126
  end
104
-
127
+
105
128
  end
106
-
129
+
107
130
  end
108
-
131
+
109
132
  end
110
- end
133
+ end
@@ -16,6 +16,9 @@ module Dummy
16
16
  # -- all .rb files in that directory are automatically loaded.
17
17
 
18
18
  config.middleware.use 'Apartment::Elevators::Subdomain'
19
+ config.middleware.use 'Apartment::Elevators::Domain'
20
+ # Our test for this middleware is using a query_string couldn't think of a better way to differentiate it from the other middleware
21
+ config.middleware.use 'Apartment::Elevators::Generic', Proc.new { |request| request.query_string.split('=').last if request.query_string.present? }
19
22
 
20
23
  # Custom directories with classes and modules you want to be autoloadable.
21
24
  config.autoload_paths += %W(#{config.root}/lib)
@@ -2,30 +2,30 @@ require 'spec_helper'
2
2
 
3
3
  shared_examples_for "a db based apartment adapter" do
4
4
  include Apartment::Spec::AdapterRequirements
5
-
5
+
6
6
  let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
7
-
7
+
8
8
  describe "#init" do
9
-
9
+
10
10
  it "should process model exclusions" do
11
11
  Apartment.configure do |config|
12
12
  config.excluded_models = ["Company"]
13
13
  end
14
-
14
+
15
15
  Apartment::Database.init
16
-
16
+
17
17
  Company.connection.object_id.should_not == ActiveRecord::Base.connection.object_id
18
18
  end
19
19
  end
20
-
20
+
21
21
  describe "#drop" do
22
22
  it "should raise an error for unknown database" do
23
23
  expect {
24
24
  subject.drop 'unknown_database'
25
25
  }.to raise_error(Apartment::DatabaseNotFound)
26
26
  end
27
- end
28
-
27
+ end
28
+
29
29
  describe "#switch" do
30
30
  it "should raise an error if database is invalid" do
31
31
  expect {
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "an apartment elevator" do
4
+
5
+ context "single request" do
6
+ it "should switch the db" do
7
+ ActiveRecord::Base.connection.schema_search_path.should_not == database1
8
+
9
+ visit(domain1)
10
+ ActiveRecord::Base.connection.schema_search_path.should == database1
11
+ end
12
+ end
13
+
14
+ context "simultaneous requests" do
15
+
16
+ let!(:c1_user_count) { api.process(database1){ (2 + rand(2)).times{ User.create } } }
17
+ let!(:c2_user_count) { api.process(database2){ (c1_user_count + 2).times{ User.create } } }
18
+
19
+ it "should fetch the correct user count for each session based on the elevator processor" do
20
+ visit(domain1)
21
+
22
+ in_new_session do |session|
23
+ session.visit(domain2)
24
+ User.count.should == c2_user_count
25
+ end
26
+
27
+ visit(domain1)
28
+ User.count.should == c1_user_count
29
+ end
30
+ end
31
+ end
@@ -2,9 +2,9 @@ require 'spec_helper'
2
2
 
3
3
  shared_examples_for "a generic apartment adapter" do
4
4
  include Apartment::Spec::AdapterRequirements
5
-
5
+
6
6
  before{ Apartment.prepend_environment = false }
7
-
7
+
8
8
  #
9
9
  # Creates happen already in our before_filter
10
10
  #
@@ -20,7 +20,7 @@ shared_examples_for "a generic apartment adapter" do
20
20
  connection.tables.should include('companies')
21
21
  end
22
22
  end
23
-
23
+
24
24
  it "should yield to block if passed and reset" do
25
25
  subject.drop(db2) # so we don't get errors on creation
26
26
 
@@ -31,20 +31,20 @@ shared_examples_for "a generic apartment adapter" do
31
31
  subject.current_database.should == db2
32
32
  User.create
33
33
  end
34
-
34
+
35
35
  subject.current_database.should_not == db2
36
36
 
37
37
  subject.process(db2){ User.count.should == @count + 1 }
38
- end
38
+ end
39
39
  end
40
-
40
+
41
41
  describe "#drop" do
42
42
  it "should remove the db" do
43
43
  subject.drop db1
44
44
  database_names.should_not include(db1)
45
- end
45
+ end
46
46
  end
47
-
47
+
48
48
  describe "#process" do
49
49
  it "should connect" do
50
50
  subject.process(db1) do
@@ -67,7 +67,7 @@ shared_examples_for "a generic apartment adapter" do
67
67
  }.to_not raise_error
68
68
  end
69
69
  end
70
-
70
+
71
71
  describe "#reset" do
72
72
  it "should reset connection" do
73
73
  subject.switch(db1)
@@ -85,7 +85,7 @@ shared_examples_for "a generic apartment adapter" do
85
85
  it "should reset connection if database is nil" do
86
86
  subject.switch
87
87
  subject.current_database.should == default_database
88
- end
88
+ end
89
89
  end
90
90
 
91
91
  describe "#current_database" do
@@ -2,22 +2,33 @@ require 'spec_helper'
2
2
 
3
3
  shared_examples_for "a schema based apartment adapter" do
4
4
  include Apartment::Spec::AdapterRequirements
5
-
5
+
6
6
  let(:schema1){ db1 }
7
7
  let(:schema2){ db2 }
8
8
  let(:public_schema){ default_database }
9
9
 
10
10
  describe "#init" do
11
-
12
- it "should process model exclusions" do
11
+
12
+ before do
13
13
  Apartment.configure do |config|
14
14
  config.excluded_models = ["Company"]
15
15
  end
16
-
16
+ end
17
+
18
+ it "should process model exclusions" do
17
19
  Apartment::Database.init
18
-
20
+
19
21
  Company.table_name.should == "public.companies"
20
22
  end
23
+
24
+ context "with a default_schema", :default_schema => true do
25
+
26
+ it "should set the proper table_name on excluded_models" do
27
+ Apartment::Database.init
28
+
29
+ Company.table_name.should == "#{default_schema}.companies"
30
+ end
31
+ end
21
32
  end
22
33
 
23
34
  #
@@ -29,7 +40,7 @@ shared_examples_for "a schema based apartment adapter" do
29
40
  connection.schema_search_path = schema1
30
41
  connection.tables.should include('companies')
31
42
  end
32
-
43
+
33
44
  it "should yield to block if passed and reset" do
34
45
  subject.drop(schema2) # so we don't get errors on creation
35
46
 
@@ -37,52 +48,61 @@ shared_examples_for "a schema based apartment adapter" do
37
48
 
38
49
  subject.create(schema2) do
39
50
  @count = User.count
40
- connection.schema_search_path.should == schema2
51
+ connection.schema_search_path.should start_with schema2
41
52
  User.create
42
53
  end
43
-
44
- connection.schema_search_path.should_not == schema2
54
+
55
+ connection.schema_search_path.should_not start_with schema2
45
56
 
46
57
  subject.process(schema2){ User.count.should == @count + 1 }
47
58
  end
48
-
49
- it "should allow numeric database names" do
50
- expect {
51
- subject.create(1234)
52
- }.to_not raise_error
53
- database_names.should include("1234")
54
- # cleanup
55
- subject.drop(1234)
59
+
60
+ context "numeric database names" do
61
+ let(:db){ 1234 }
62
+ it "should allow them" do
63
+ expect {
64
+ subject.create(db)
65
+ }.to_not raise_error
66
+ database_names.should include(db.to_s)
67
+ end
68
+
69
+ after{ subject.drop(db) }
56
70
  end
57
-
71
+
58
72
  end
59
-
73
+
60
74
  describe "#drop" do
61
75
  it "should raise an error for unknown database" do
62
76
  expect {
63
77
  subject.drop "unknown_database"
64
78
  }.to raise_error(Apartment::SchemaNotFound)
65
79
  end
66
-
67
- it "should be able to drop numeric dbs" do
68
- subject.create(1234)
69
- expect {
70
- subject.drop(1234)
71
- }.to_not raise_error
72
- database_names.should_not include("1234")
80
+
81
+ context "numeric database names" do
82
+ let(:db){ 1234 }
83
+
84
+ it "should be able to drop them" do
85
+ subject.create(db)
86
+ expect {
87
+ subject.drop(db)
88
+ }.to_not raise_error
89
+ database_names.should_not include(db.to_s)
90
+ end
91
+
92
+ after { subject.drop(db) rescue nil }
73
93
  end
74
94
  end
75
95
 
76
96
  describe "#process" do
77
97
  it "should connect" do
78
98
  subject.process(schema1) do
79
- connection.schema_search_path.should == schema1
99
+ connection.schema_search_path.should start_with schema1
80
100
  end
81
101
  end
82
102
 
83
103
  it "should reset" do
84
104
  subject.process(schema1)
85
- connection.schema_search_path.should == public_schema
105
+ connection.schema_search_path.should start_with public_schema
86
106
  end
87
107
  end
88
108
 
@@ -90,33 +110,93 @@ shared_examples_for "a schema based apartment adapter" do
90
110
  it "should reset connection" do
91
111
  subject.switch(schema1)
92
112
  subject.reset
93
- connection.schema_search_path.should == public_schema
113
+ connection.schema_search_path.should start_with public_schema
114
+ end
115
+
116
+ context "with default_schema", :default_schema => true do
117
+ it "should reset to the default schema" do
118
+ subject.switch(schema1)
119
+ subject.reset
120
+ connection.schema_search_path.should start_with default_schema
121
+ end
122
+ end
123
+
124
+ context "persistent_schemas", :persistent_schemas => true do
125
+ before do
126
+ subject.switch(schema1)
127
+ subject.reset
128
+ end
129
+
130
+ it "maintains the persistent schemas in the schema_search_path" do
131
+ connection.schema_search_path.should end_with persistent_schemas.join(', ')
132
+ end
133
+
134
+ context "with default_schema", :default_schema => true do
135
+ it "prioritizes the switched schema to front of schema_search_path" do
136
+ subject.reset # need to re-call this as the default_schema wasn't set at the time that the above reset ran
137
+ connection.schema_search_path.should start_with default_schema
138
+ end
139
+ end
94
140
  end
95
141
  end
96
142
 
97
143
  describe "#switch" do
98
144
  it "should connect to new schema" do
99
145
  subject.switch(schema1)
100
- connection.schema_search_path.should == schema1
146
+ connection.schema_search_path.should start_with schema1
101
147
  end
102
148
 
103
149
  it "should reset connection if database is nil" do
104
150
  subject.switch
105
151
  connection.schema_search_path.should == public_schema
106
152
  end
107
-
153
+
108
154
  it "should raise an error if schema is invalid" do
109
155
  expect {
110
156
  subject.switch 'unknown_schema'
111
157
  }.to raise_error(Apartment::SchemaNotFound)
112
158
  end
113
-
114
- it "should connect to numeric dbs" do
115
- subject.create(1234)
116
- expect {
117
- subject.switch(1234)
118
- }.to_not raise_error
119
- subject.drop(1234)
159
+
160
+ context "numeric databases" do
161
+ let(:db){ 1234 }
162
+
163
+ it "should connect to them" do
164
+ subject.create(db)
165
+ expect {
166
+ subject.switch(db)
167
+ }.to_not raise_error
168
+
169
+ connection.schema_search_path.should start_with db.to_s
170
+ end
171
+
172
+ after{ subject.drop(db) }
173
+ end
174
+
175
+ describe "with default_schema specified", :default_schema => true do
176
+ before do
177
+ subject.switch(schema1)
178
+ end
179
+
180
+ it "should switch out the default schema rather than public" do
181
+ connection.schema_search_path.should_not include default_schema
182
+ end
183
+
184
+ it "should still switch to the switched schema" do
185
+ connection.schema_search_path.should start_with schema1
186
+ end
187
+ end
188
+
189
+ context "persistent_schemas", :persistent_schemas => true do
190
+
191
+ before{ subject.switch(schema1) }
192
+
193
+ it "maintains the persistent schemas in the schema_search_path" do
194
+ connection.schema_search_path.should end_with persistent_schemas.join(', ')
195
+ end
196
+
197
+ it "prioritizes the switched schema to front of schema_search_path" do
198
+ connection.schema_search_path.should start_with schema1
199
+ end
120
200
  end
121
201
  end
122
202
 
@@ -125,6 +205,13 @@ shared_examples_for "a schema based apartment adapter" do
125
205
  subject.switch(schema1)
126
206
  subject.current_database.should == schema1
127
207
  end
208
+
209
+ context "persistent_schemas", :persistent_schemas => true do
210
+ it "should exlude persistent_schemas" do
211
+ subject.switch(schema1)
212
+ subject.current_database.should == schema1
213
+ end
214
+ end
128
215
  end
129
216
 
130
217
  end