apartment 0.15.0 → 0.16.0

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