apartment 0.13.0.1 → 0.13.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/.rvmrc +1 -1
- data/HISTORY.md +28 -29
- data/apartment.gemspec +12 -15
- data/lib/apartment.rb +21 -34
- data/lib/apartment/adapters/abstract_adapter.rb +64 -64
- data/lib/apartment/adapters/postgresql_adapter.rb +52 -51
- data/lib/apartment/database.rb +20 -22
- data/lib/apartment/delayed_job/hooks.rb +5 -5
- data/lib/apartment/elevators/subdomain.rb +11 -27
- data/lib/apartment/version.rb +1 -1
- data/spec/adapters/postgresql_adapter_spec.rb +29 -38
- data/spec/apartment_spec.rb +1 -1
- data/spec/dummy/db/schema.rb +1 -7
- data/spec/integration/database_integration_spec.rb +44 -44
- data/spec/integration/middleware/subdomain_elevator_spec.rb +15 -15
- data/spec/spec_helper.rb +3 -7
- data/spec/support/apartment_helpers.rb +8 -8
- data/spec/tasks/apartment_rake_spec.rb +1 -0
- data/spec/unit/config_spec.rb +18 -48
- data/spec/unit/middleware/subdomain_elevator_spec.rb +8 -11
- data/spec/unit/reloader_spec.rb +6 -6
- metadata +92 -107
@@ -1,114 +1,115 @@
|
|
1
1
|
module Apartment
|
2
|
-
|
2
|
+
|
3
3
|
module Database
|
4
|
-
|
4
|
+
|
5
5
|
def self.postgresql_adapter(config)
|
6
|
-
Apartment.use_postgres_schemas ?
|
6
|
+
Apartment.use_postgres_schemas ?
|
7
7
|
Adapters::PostgresqlSchemaAdapter.new(config, :schema_search_path => ActiveRecord::Base.connection.schema_search_path) :
|
8
8
|
Adapters::PostgresqlAdapter.new(config)
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
module Adapters
|
14
|
-
|
14
|
+
|
15
15
|
# Default adapter when not using Postgresql Schemas
|
16
16
|
class PostgresqlAdapter < AbstractAdapter
|
17
|
-
|
17
|
+
|
18
18
|
protected
|
19
|
-
|
19
|
+
|
20
20
|
# Connect to new database
|
21
21
|
# Abstract adapter will catch generic ActiveRecord error
|
22
22
|
# Catch specific adapter errors here
|
23
|
-
#
|
23
|
+
#
|
24
24
|
# @param {String} database Database name
|
25
|
-
#
|
25
|
+
#
|
26
26
|
def connect_to_new(database)
|
27
27
|
super
|
28
|
-
rescue PGError
|
28
|
+
rescue PGError => e
|
29
29
|
raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
|
30
|
-
|
31
|
-
|
30
|
+
end
|
31
|
+
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
# Separate Adapter for Postgresql when using schemas
|
35
35
|
class PostgresqlSchemaAdapter < AbstractAdapter
|
36
|
-
|
36
|
+
|
37
37
|
# Get the current schema search path
|
38
|
-
#
|
38
|
+
#
|
39
39
|
# @return {String} current schema search path
|
40
|
-
#
|
40
|
+
#
|
41
41
|
def current_database
|
42
42
|
ActiveRecord::Base.connection.schema_search_path
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
# Drop the database schema
|
46
|
-
#
|
46
|
+
#
|
47
47
|
# @param {String} database Database (schema) to drop
|
48
|
-
#
|
48
|
+
#
|
49
49
|
def drop(database)
|
50
50
|
ActiveRecord::Base.connection.execute("DROP SCHEMA #{database} CASCADE")
|
51
|
-
|
52
|
-
rescue ActiveRecord::StatementInvalid
|
51
|
+
|
52
|
+
rescue ActiveRecord::StatementInvalid => e
|
53
53
|
raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
|
54
54
|
end
|
55
55
|
|
56
56
|
# Reset search path to default search_path
|
57
57
|
# Set the table_name to always use the public namespace for excluded models
|
58
|
-
#
|
58
|
+
#
|
59
59
|
def process_excluded_models
|
60
|
-
|
60
|
+
Apartment.excluded_models.each do |excluded_model|
|
61
61
|
# Note that due to rails reloading, we now take string references to classes rather than
|
62
62
|
# actual object references. This way when we contantize, we always get the proper class reference
|
63
63
|
if excluded_model.is_a? Class
|
64
64
|
warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
|
65
65
|
excluded_model = excluded_model.name
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
excluded_model.constantize.tap do |klass|
|
69
|
-
# some models (such as delayed_job) seem to load and cache their column names before this,
|
69
|
+
# some models (such as delayed_job) seem to load and cache their column names before this,
|
70
70
|
# so would never get the public prefix, so reset first
|
71
|
-
|
71
|
+
klass.reset_column_information
|
72
72
|
|
73
73
|
# Ensure that if a schema *was* set, we override
|
74
|
-
|
74
|
+
table_name = klass.table_name.split('.', 2).last
|
75
75
|
|
76
76
|
# Not sure why, but Delayed::Job somehow ignores table_name_prefix... so we'll just manually set table name instead
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
klass.table_name = "public.#{table_name}"
|
78
|
+
end
|
79
|
+
end
|
80
80
|
end
|
81
|
-
|
81
|
+
|
82
82
|
# Reset schema search path to the default schema_search_path
|
83
|
-
#
|
83
|
+
#
|
84
84
|
# @return {String} default schema search path
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
|
85
|
+
#
|
86
|
+
def reset
|
87
|
+
ActiveRecord::Base.connection.schema_search_path = @defaults[:schema_search_path]
|
88
|
+
end
|
89
89
|
|
90
|
-
|
90
|
+
protected
|
91
91
|
|
92
|
-
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
92
|
+
# Set schema search path to new schema
|
93
|
+
#
|
94
|
+
def connect_to_new(database = nil)
|
95
|
+
return reset if database.nil?
|
96
|
+
ActiveRecord::Base.connection.clear_cache!
|
97
|
+
ActiveRecord::Base.connection.schema_search_path = database
|
97
98
|
|
98
99
|
rescue ActiveRecord::StatementInvalid => e
|
99
100
|
raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
|
100
|
-
|
101
|
+
end
|
101
102
|
|
102
103
|
# Create the new schema
|
103
|
-
#
|
104
|
-
|
105
|
-
|
104
|
+
#
|
105
|
+
def create_database(database)
|
106
|
+
ActiveRecord::Base.connection.execute("CREATE SCHEMA #{database}")
|
106
107
|
|
107
|
-
|
108
|
-
|
108
|
+
rescue ActiveRecord::StatementInvalid => e
|
109
|
+
raise SchemaExists, "The schema #{database} already exists."
|
109
110
|
end
|
110
|
-
|
111
|
+
|
111
112
|
end
|
112
|
-
|
113
|
+
|
113
114
|
end
|
114
115
|
end
|
data/lib/apartment/database.rb
CHANGED
@@ -1,59 +1,57 @@
|
|
1
1
|
require 'active_support/core_ext/module/delegation'
|
2
2
|
|
3
3
|
module Apartment
|
4
|
-
|
4
|
+
|
5
5
|
# The main entry point to Apartment functions
|
6
|
-
|
7
|
-
|
6
|
+
module Database
|
7
|
+
|
8
8
|
extend self
|
9
9
|
|
10
|
-
delegate :create, :current_database, :
|
11
|
-
|
12
|
-
attr_writer :config
|
10
|
+
delegate :create, :current_database, :process, :process_excluded_models, :reset, :seed, :switch, :to => :adapter
|
13
11
|
|
14
12
|
# Initialize Apartment config options such as excluded_models
|
15
|
-
#
|
16
|
-
|
13
|
+
#
|
14
|
+
def init
|
17
15
|
process_excluded_models
|
18
16
|
end
|
19
|
-
|
17
|
+
|
20
18
|
# Fetch the proper multi-tenant adapter based on Rails config
|
21
|
-
#
|
19
|
+
#
|
22
20
|
# @return {subclass of Apartment::AbstractAdapter}
|
23
|
-
#
|
21
|
+
#
|
24
22
|
def adapter
|
25
23
|
@adapter ||= begin
|
26
24
|
adapter_method = "#{config[:adapter]}_adapter"
|
27
|
-
|
28
|
-
|
25
|
+
|
26
|
+
begin
|
29
27
|
require "apartment/adapters/#{adapter_method}"
|
30
|
-
rescue LoadError
|
28
|
+
rescue LoadError => e
|
31
29
|
raise "The adapter `#{config[:adapter]}` is not yet supported"
|
32
30
|
end
|
33
31
|
|
34
32
|
unless respond_to?(adapter_method)
|
35
33
|
raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
|
36
34
|
end
|
37
|
-
|
35
|
+
|
38
36
|
send(adapter_method, config)
|
39
37
|
end
|
40
38
|
end
|
41
|
-
|
39
|
+
|
42
40
|
# Reset config and adapter so they are regenerated
|
43
|
-
#
|
41
|
+
#
|
44
42
|
def reload!
|
45
43
|
@adapter = nil
|
46
44
|
@config = nil
|
47
45
|
end
|
48
|
-
|
46
|
+
|
49
47
|
private
|
50
|
-
|
48
|
+
|
51
49
|
# Fetch the rails database configuration
|
52
|
-
#
|
50
|
+
#
|
53
51
|
def config
|
54
52
|
@config ||= Rails.configuration.database_configuration[Rails.env].symbolize_keys
|
55
53
|
end
|
56
|
-
|
54
|
+
|
57
55
|
end
|
58
|
-
|
56
|
+
|
59
57
|
end
|
@@ -3,22 +3,22 @@ require 'apartment/delayed_job/enqueue'
|
|
3
3
|
module Apartment
|
4
4
|
module Delayed
|
5
5
|
module Job
|
6
|
-
|
6
|
+
|
7
7
|
# Before and after hooks for performing Delayed Jobs within a particular apartment database
|
8
8
|
# Include these in your delayed jobs models and make sure provide a @database attr that will be serialized by DJ
|
9
9
|
# Note also that any models that are being serialized need the Apartment::Delayed::Requirements module mixed in to it
|
10
10
|
module Hooks
|
11
|
-
|
11
|
+
|
12
12
|
attr_accessor :database
|
13
|
-
|
13
|
+
|
14
14
|
def before(job)
|
15
15
|
Apartment::Database.switch(job.payload_object.database) if job.payload_object.database
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def after
|
19
19
|
Apartment::Database.reset
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -3,41 +3,25 @@ module Apartment
|
|
3
3
|
# Provides a rack based db switching solution based on subdomains
|
4
4
|
# Assumes that database name should match subdomain
|
5
5
|
class Subdomain
|
6
|
-
|
6
|
+
|
7
7
|
def initialize(app)
|
8
8
|
@app = app
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
def call(env)
|
12
|
-
|
13
|
-
|
14
|
-
database = subdomain(
|
15
|
-
|
12
|
+
request = ActionDispatch::Request.new(env)
|
13
|
+
|
14
|
+
database = subdomain(request)
|
15
|
+
|
16
16
|
Apartment::Database.switch database if database
|
17
|
-
|
17
|
+
|
18
18
|
@app.call(env)
|
19
19
|
end
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# *Almost* a direct ripoff of ActionDispatch::Request subdomain methods
|
24
|
-
|
25
|
-
# Only care about the first subdomain for the database name
|
26
|
-
def subdomain(host)
|
27
|
-
subdomains(host).first
|
20
|
+
|
21
|
+
def subdomain(request)
|
22
|
+
request.subdomain.present? && request.subdomain || nil
|
28
23
|
end
|
29
|
-
|
30
|
-
# Assuming tld_length of 1, might need to make this configurable in Apartment in the future for things like .co.uk
|
31
|
-
def subdomains(host, tld_length = 1)
|
32
|
-
return [] unless named_host?(host)
|
33
|
-
|
34
|
-
host.split('.')[0..-(tld_length + 2)]
|
35
|
-
end
|
36
|
-
|
37
|
-
def named_host?(host)
|
38
|
-
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
39
|
-
end
|
40
|
-
|
24
|
+
|
41
25
|
end
|
42
26
|
end
|
43
27
|
end
|
data/lib/apartment/version.rb
CHANGED
@@ -2,76 +2,67 @@ require 'spec_helper'
|
|
2
2
|
require 'apartment/adapters/postgresql_adapter' # specific adapters get dynamically loaded based on adapter name, so we must manually require here
|
3
3
|
|
4
4
|
describe Apartment::Adapters::PostgresqlAdapter do
|
5
|
-
|
5
|
+
|
6
6
|
before do
|
7
7
|
ActiveRecord::Base.establish_connection Apartment::Test.config['connections']['postgresql']
|
8
8
|
@schema_search_path = ActiveRecord::Base.connection.schema_search_path
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
after do
|
12
12
|
ActiveRecord::Base.clear_all_connections!
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
context "using schemas" do
|
16
|
-
|
16
|
+
|
17
17
|
let(:schema){ 'first_db_schema' }
|
18
18
|
let(:schema2){ 'another_db_schema' }
|
19
19
|
let(:database_names){ ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect{|row| row['nspname']} }
|
20
|
-
|
20
|
+
|
21
21
|
subject{ Apartment::Database.postgresql_adapter Apartment::Test.config['connections']['postgresql'].symbolize_keys }
|
22
|
-
|
22
|
+
|
23
23
|
before do
|
24
24
|
Apartment.use_postgres_schemas = true
|
25
25
|
subject.create(schema)
|
26
26
|
subject.create(schema2)
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
after do
|
30
30
|
# sometimes we manually drop these schemas in testing, dont' care if we can't drop hence rescue
|
31
|
-
subject.drop(schema) rescue true
|
31
|
+
subject.drop(schema) rescue true
|
32
32
|
subject.drop(schema2) rescue true
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
describe "#create" do
|
36
|
-
|
36
|
+
|
37
37
|
it "should create the new schema" do
|
38
38
|
database_names.should include(schema)
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
it "should load schema.rb to new schema" do
|
42
42
|
ActiveRecord::Base.connection.schema_search_path = schema
|
43
43
|
ActiveRecord::Base.connection.tables.should include('companies')
|
44
44
|
end
|
45
|
-
|
46
|
-
it "should not load schema.rb if load_schema is false" do
|
47
|
-
Apartment.load_schema = false
|
48
|
-
subject.create("schemax") do
|
49
|
-
ActiveRecord::Base.connection.tables.should_not include('companies')
|
50
|
-
end
|
51
|
-
# Cleanup
|
52
|
-
subject.drop("schemax")
|
53
|
-
end
|
54
|
-
|
45
|
+
|
55
46
|
it "should reset connection when finished" do
|
56
47
|
ActiveRecord::Base.connection.schema_search_path.should_not == schema
|
57
48
|
end
|
58
|
-
|
49
|
+
|
59
50
|
it "should yield to block if passed" do
|
60
|
-
Apartment::Test.migrate # ensure we have latest schema in the public
|
51
|
+
Apartment::Test.migrate # ensure we have latest schema in the public
|
61
52
|
subject.drop(schema2) # so we don't get errors on creation
|
62
|
-
|
53
|
+
|
63
54
|
@count = 0 # set our variable so its visible in and outside of blocks
|
64
|
-
|
55
|
+
|
65
56
|
subject.create(schema2) do
|
66
57
|
@count = User.count
|
67
58
|
ActiveRecord::Base.connection.schema_search_path.should == schema2
|
68
59
|
User.create
|
69
60
|
end
|
70
|
-
|
61
|
+
|
71
62
|
subject.process(schema2){ User.count.should == @count + 1 }
|
72
63
|
end
|
73
64
|
end
|
74
|
-
|
65
|
+
|
75
66
|
describe "#drop" do
|
76
67
|
|
77
68
|
it "should delete the database" do
|
@@ -87,31 +78,31 @@ describe Apartment::Adapters::PostgresqlAdapter do
|
|
87
78
|
}.to raise_error(Apartment::SchemaNotFound)
|
88
79
|
end
|
89
80
|
end
|
90
|
-
|
91
|
-
|
81
|
+
|
82
|
+
|
92
83
|
describe "#process" do
|
93
84
|
it "should connect" do
|
94
85
|
subject.process(schema) do
|
95
86
|
ActiveRecord::Base.connection.schema_search_path.should == schema
|
96
87
|
end
|
97
88
|
end
|
98
|
-
|
89
|
+
|
99
90
|
it "should reset" do
|
100
91
|
subject.process(schema)
|
101
92
|
ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
|
102
93
|
end
|
103
|
-
|
94
|
+
|
104
95
|
# We're often finding when using Apartment in tests, the `current_database` (ie the previously attached to schema)
|
105
96
|
# gets dropped, but process will try to return to that schema in a test. We should just reset if it doesnt exist
|
106
97
|
it "should not throw exception if current_database (schema) is no longer accessible" do
|
107
98
|
subject.switch(schema2)
|
108
|
-
|
99
|
+
|
109
100
|
expect {
|
110
101
|
subject.process(schema){ subject.drop(schema2) }
|
111
102
|
}.to_not raise_error(Apartment::SchemaNotFound)
|
112
103
|
end
|
113
104
|
end
|
114
|
-
|
105
|
+
|
115
106
|
describe "#reset" do
|
116
107
|
it "should reset connection" do
|
117
108
|
subject.switch(schema)
|
@@ -119,28 +110,28 @@ describe Apartment::Adapters::PostgresqlAdapter do
|
|
119
110
|
ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
|
120
111
|
end
|
121
112
|
end
|
122
|
-
|
113
|
+
|
123
114
|
describe "#switch" do
|
124
115
|
it "should connect to new schema" do
|
125
116
|
subject.switch(schema)
|
126
117
|
ActiveRecord::Base.connection.schema_search_path.should == schema
|
127
118
|
end
|
128
|
-
|
119
|
+
|
129
120
|
it "should reset connection if database is nil" do
|
130
121
|
subject.switch
|
131
122
|
ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
|
132
123
|
end
|
133
124
|
end
|
134
|
-
|
125
|
+
|
135
126
|
describe "#current_database" do
|
136
127
|
it "should return the current schema name" do
|
137
128
|
subject.switch(schema)
|
138
129
|
subject.current_database.should == schema
|
139
130
|
end
|
140
131
|
end
|
141
|
-
|
132
|
+
|
142
133
|
end
|
143
|
-
|
134
|
+
|
144
135
|
context "using databases" do
|
145
136
|
# TODO
|
146
137
|
end
|