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.
- data/.pryrc +3 -0
- data/.travis.yml +5 -0
- data/HISTORY.md +8 -0
- data/README.md +66 -14
- data/Rakefile +11 -10
- data/apartment.gemspec +6 -5
- data/lib/apartment.rb +21 -3
- data/lib/apartment/adapters/{mysql_adapter.rb → mysql2_adapter.rb} +7 -7
- data/lib/apartment/adapters/postgresql_adapter.rb +26 -15
- data/lib/apartment/database.rb +3 -0
- data/lib/apartment/elevators/domain.rb +18 -0
- data/lib/apartment/elevators/generic.rb +27 -0
- data/lib/apartment/elevators/subdomain.rb +5 -19
- data/lib/apartment/version.rb +1 -1
- data/spec/adapters/{mysql_adapter_spec.rb → mysql2_adapter_spec.rb} +7 -7
- data/spec/adapters/postgresql_adapter_spec.rb +4 -4
- data/spec/config/database.yml +2 -1
- data/spec/database_spec.rb +49 -26
- data/spec/dummy/config/application.rb +3 -0
- data/spec/examples/db_adapter_examples.rb +8 -8
- data/spec/examples/elevator_examples.rb +31 -0
- data/spec/examples/generic_adapter_examples.rb +10 -10
- data/spec/examples/schema_adapter_examples.rb +125 -38
- data/spec/integration/delayed_job_integration_spec.rb +1 -1
- data/spec/integration/middleware/domain_elevator_spec.rb +9 -0
- data/spec/integration/middleware/generic_elevator_spec.rb +10 -0
- data/spec/integration/middleware/subdomain_elevator_spec.rb +6 -49
- data/spec/spec_helper.rb +5 -2
- data/spec/support/apartment_helpers.rb +13 -10
- data/spec/support/contexts.rb +54 -0
- data/spec/support/requirements.rb +4 -4
- data/spec/unit/config_spec.rb +0 -4
- data/spec/unit/middleware/domain_elevator_spec.rb +26 -0
- data/spec/unit/middleware/subdomain_elevator_spec.rb +7 -7
- metadata +119 -108
data/.pryrc
ADDED
data/.travis.yml
ADDED
data/HISTORY.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
# 0.16.0
|
2
|
+
* June 1, 2012
|
3
|
+
|
4
|
+
- Apartment now supports a default_schema to be set, rather than relying on ActiveRecord's default schema_search_path
|
5
|
+
- Additional schemas can always be maintained in the schema_search_path by configuring persistent_schemas
|
6
|
+
- There is now a full domain based elevator to switch dbs based on the whole domain [lcowell]
|
7
|
+
- There is now a generic elevator that takes a Proc to switch dbs based on the return value of that proc.
|
8
|
+
|
1
9
|
# 0.15.0
|
2
10
|
* March 18, 2012
|
3
11
|
|
data/README.md
CHANGED
@@ -60,8 +60,9 @@ database, call switch with no arguments.
|
|
60
60
|
|
61
61
|
You can have Apartment route to the appropriate database by adding some Rack middleware.
|
62
62
|
Apartment can support many different "Elevators" that can take care of this routing to your data.
|
63
|
-
|
64
|
-
|
63
|
+
|
64
|
+
**Switch on subdomain**
|
65
|
+
In house, we use the subdomain elevator, which analyzes the subdomain of the request and switches to a database schema of the same name. It can be used like so:
|
65
66
|
|
66
67
|
# application.rb
|
67
68
|
module My Application
|
@@ -71,6 +72,29 @@ to a database schema of the same name. It can be used like so:
|
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
75
|
+
**Switch on domain**
|
76
|
+
To switch based on full domain (excluding subdomains *ie 'www'* and top level domains *ie '.com'* ) use the following:
|
77
|
+
|
78
|
+
# application.rb
|
79
|
+
module My Application
|
80
|
+
class Application < Rails::Application
|
81
|
+
|
82
|
+
config.middleware.use 'Apartment::Elevators::Domain'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
**Custom Elevator**
|
87
|
+
A Generic Elevator exists that allows you to pass a `Proc` (or anything that responds to `call`) to the middleware. This Object will be passed in an `ActionDispatch::Request` object when called for you to do your magic. Apartment will use the return value of this proc to switch to the appropriate database. Use like so:
|
88
|
+
|
89
|
+
# application.rb
|
90
|
+
module My Application
|
91
|
+
class Application < Rails::Application
|
92
|
+
# Obviously not a contrived example
|
93
|
+
config.middleware.use 'Apartment::Elevators::Generic', Proc.new { |request| request.host.reverse }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
74
98
|
## Config
|
75
99
|
|
76
100
|
The following config options should be set up in a Rails initializer such as:
|
@@ -85,20 +109,33 @@ To set config options, add this to your initializer:
|
|
85
109
|
|
86
110
|
### Excluding models
|
87
111
|
|
88
|
-
If you have some models that should always access the 'root' database, you can specify this by configuring
|
89
|
-
Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
|
112
|
+
If you have some models that should always access the 'root' database, you can specify this by configuring Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
|
90
113
|
|
91
114
|
config.excluded_models = ["User", "Company"] # these models will not be multi-tenanted, but remain in the global (public) namespace
|
92
115
|
|
93
116
|
Note that a string representation of the model name is now the standard so that models are properly constantized when reloaded in development
|
94
117
|
|
95
|
-
###
|
118
|
+
### Postgresql Schemas
|
96
119
|
|
97
|
-
|
98
|
-
|
99
|
-
and test environments. If you wish to turn this option off in production, you could do something like:
|
120
|
+
**Providing a Different default_schema**
|
121
|
+
By default, ActiveRecord will use `"$user", public` as the default `schema_search_path`. This can be modified if you wish to use a different default schema be setting:
|
100
122
|
|
101
|
-
config.
|
123
|
+
config.default_schema = "some_other_schema"
|
124
|
+
|
125
|
+
With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Database` will return to this schema also
|
126
|
+
|
127
|
+
**Persistent Schemas**
|
128
|
+
Apartment will normally just switch the `schema_search_path` whole hog to the one passed in. This can lead to problems if you want other schemas to always be searched as well. Enter `persistent_schemas`. You can configure a list of other schemas that will always remain in the search path, while the default gets swapped out:
|
129
|
+
|
130
|
+
config.persistent_schemas = ['some', 'other', 'schemas']
|
131
|
+
|
132
|
+
This has numerous useful applications. [Hstore](http://www.postgresql.org/docs/9.1/static/hstore.html), for instance, is a popular storage engine for Postgresql. In order to use Hstore, you have to install to a specific schema and have that always in the `schema_search_path`. This could be achieved like so:
|
133
|
+
|
134
|
+
# In a rake task, or on the console...
|
135
|
+
ActiveRecord::Base.connection.execute("CREATE SCHEMA hstore; CREATE EXTENSION HSTORE SCHEMA hstore")
|
136
|
+
|
137
|
+
# configure Apartment to maintain the `hstore` schema in the `schema_search_path`
|
138
|
+
config.persistent_schemas = ['hstore']
|
102
139
|
|
103
140
|
### Managing Migrations
|
104
141
|
|
@@ -119,7 +156,15 @@ You can then migration your databases using the rake task:
|
|
119
156
|
This basically invokes `Apartment::Database.migrate(#{db_name})` for each database name supplied
|
120
157
|
from `Apartment.database_names`
|
121
158
|
|
122
|
-
###
|
159
|
+
### Handling Environments
|
160
|
+
|
161
|
+
By default, when not using postgresql schemas, Apartment will prepend the environment to the database name
|
162
|
+
to ensure there is no conflict between your environments. This is mainly for the benefit of your development
|
163
|
+
and test environments. If you wish to turn this option off in production, you could do something like:
|
164
|
+
|
165
|
+
config.prepend_environment = !Rails.env.production?
|
166
|
+
|
167
|
+
## Delayed::Job
|
123
168
|
|
124
169
|
If using Rails ~> 3.2, you *must* use `delayed_job ~> 3.0`. It has better Rails 3 support plus has some major changes that affect the serialization of models.
|
125
170
|
I haven't been able to get `psych` working whatsoever as the YAML parser, so to get things to work properly, you must explicitly set the parser to `syck` *before* requiring `delayed_job`
|
@@ -139,18 +184,25 @@ In order to make ActiveRecord models play nice with DJ and Apartment, include `A
|
|
139
184
|
include Apartment::Delayed::Requirements
|
140
185
|
end
|
141
186
|
|
187
|
+
Any classes that are being used as a Delayed::Job Job need to include the `Apartment::Delayed::Job::Hooks` module into the class. This ensures that when a job runs, it switches to the appropriate tenant before performing its task. It is also required (manually at the moment) that you set a `@database` attribute on your job so the hooks know what tennant to switch to
|
188
|
+
|
142
189
|
class SomeDJ
|
143
190
|
|
144
|
-
|
145
|
-
|
146
|
-
|
191
|
+
include Apartment::Delayed::Job::Hooks
|
192
|
+
|
193
|
+
def initialize
|
194
|
+
@database = Apartment::Database.current_database
|
147
195
|
end
|
148
196
|
|
149
197
|
def perform
|
150
|
-
# do some stuff
|
198
|
+
# do some stuff (will automatically switch to @database before performing and switch back after)
|
151
199
|
end
|
152
200
|
end
|
153
201
|
|
202
|
+
All jobs *must* stored in the global (public) namespace, so add it to the list of excluded models:
|
203
|
+
|
204
|
+
config.excluded_models = ["Delayed::Job"]
|
205
|
+
|
154
206
|
## Development
|
155
207
|
|
156
208
|
* The Local setup for development assumes that a root user with no password exists for both mysql and postgresl
|
data/Rakefile
CHANGED
@@ -7,16 +7,17 @@ require "rspec/core/rake_task"
|
|
7
7
|
|
8
8
|
RSpec::Core::RakeTask.new(:spec => "db:test:prepare") do |spec|
|
9
9
|
spec.pattern = "spec/**/*_spec.rb"
|
10
|
+
# spec.rspec_opts = '--order rand:16996'
|
10
11
|
end
|
11
12
|
|
12
13
|
namespace :spec do
|
13
|
-
|
14
|
+
|
14
15
|
[:tasks, :unit, :adapters, :integration].each do |type|
|
15
16
|
RSpec::Core::RakeTask.new(type => :spec) do |spec|
|
16
17
|
spec.pattern = "spec/#{type}/**/*_spec.rb"
|
17
18
|
end
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
end
|
21
22
|
|
22
23
|
task :default => :spec
|
@@ -30,39 +31,39 @@ end
|
|
30
31
|
namespace :postgres do
|
31
32
|
require 'active_record'
|
32
33
|
require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
|
33
|
-
|
34
|
+
|
34
35
|
desc 'Build the PostgreSQL test databases'
|
35
36
|
task :build_db do
|
36
|
-
%x{ createdb -E UTF8 #{pg_config['database']} } rescue "test db already exists"
|
37
|
+
%x{ createdb -E UTF8 #{pg_config['database']} -Upostgres } rescue "test db already exists"
|
37
38
|
ActiveRecord::Base.establish_connection pg_config
|
38
39
|
ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
|
39
40
|
end
|
40
|
-
|
41
|
+
|
41
42
|
desc "drop the PostgreSQL test database"
|
42
43
|
task :drop_db do
|
43
44
|
puts "dropping database #{pg_config['database']}"
|
44
|
-
%x{ dropdb #{pg_config['database']} }
|
45
|
+
%x{ dropdb #{pg_config['database']} -Upostgres }
|
45
46
|
end
|
46
|
-
|
47
|
+
|
47
48
|
end
|
48
49
|
|
49
50
|
namespace :mysql do
|
50
51
|
require 'active_record'
|
51
52
|
require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
|
52
|
-
|
53
|
+
|
53
54
|
desc 'Build the MySQL test databases'
|
54
55
|
task :build_db do
|
55
56
|
%x{ mysqladmin -u root create #{my_config['database']} } rescue "test db already exists"
|
56
57
|
ActiveRecord::Base.establish_connection my_config
|
57
58
|
ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
|
58
59
|
end
|
59
|
-
|
60
|
+
|
60
61
|
desc "drop the MySQL test database"
|
61
62
|
task :drop_db do
|
62
63
|
puts "dropping database #{my_config['database']}"
|
63
64
|
%x{ mysqladmin -u root drop #{my_config['database']} --force}
|
64
65
|
end
|
65
|
-
|
66
|
+
|
66
67
|
end
|
67
68
|
|
68
69
|
# TODO clean this up
|
data/apartment.gemspec
CHANGED
@@ -17,15 +17,16 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.licenses = ["MIT"]
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
s.rubygems_version = %q{1.3.7}
|
20
|
-
|
20
|
+
|
21
21
|
s.add_dependency 'activerecord', '>= 3.1.2' # must be >= 3.1.2 due to bug in prepared_statements
|
22
|
-
s.add_dependency 'rack', '
|
23
|
-
|
22
|
+
s.add_dependency 'rack', '>= 1.3.6'
|
23
|
+
|
24
|
+
s.add_development_dependency 'pry', '~> 0.9.9'
|
24
25
|
s.add_development_dependency 'rails', '>= 3.1.2'
|
25
26
|
s.add_development_dependency 'rake', '~> 0.9.2'
|
26
27
|
s.add_development_dependency 'sqlite3'
|
27
|
-
s.add_development_dependency 'rspec', '~> 2.
|
28
|
-
s.add_development_dependency 'rspec-rails', '~> 2.
|
28
|
+
s.add_development_dependency 'rspec', '~> 2.10.0'
|
29
|
+
s.add_development_dependency 'rspec-rails', '~> 2.10.0'
|
29
30
|
s.add_development_dependency 'capybara', '~> 1.0.0'
|
30
31
|
s.add_development_dependency 'pg', '>= 0.11.0'
|
31
32
|
s.add_development_dependency 'mysql2', '~> 0.3.10'
|
data/lib/apartment.rb
CHANGED
@@ -3,8 +3,11 @@ require 'apartment/railtie' if defined?(Rails)
|
|
3
3
|
module Apartment
|
4
4
|
|
5
5
|
class << self
|
6
|
-
|
7
|
-
|
6
|
+
ACCESSOR_METHODS = [:use_postgres_schemas, :seed_after_create, :prepend_environment]
|
7
|
+
WRITER_METHODS = [:database_names, :excluded_models, :default_schema, :persistent_schemas]
|
8
|
+
|
9
|
+
attr_accessor(*ACCESSOR_METHODS)
|
10
|
+
attr_writer(*WRITER_METHODS)
|
8
11
|
|
9
12
|
# configure apartment with available options
|
10
13
|
def configure
|
@@ -21,6 +24,19 @@ module Apartment
|
|
21
24
|
@excluded_models || []
|
22
25
|
end
|
23
26
|
|
27
|
+
def default_schema
|
28
|
+
@default_schema || "public"
|
29
|
+
end
|
30
|
+
|
31
|
+
def persistent_schemas
|
32
|
+
@persistent_schemas || []
|
33
|
+
end
|
34
|
+
|
35
|
+
# Reset all the config for Apartment
|
36
|
+
def reset
|
37
|
+
(ACCESSOR_METHODS + WRITER_METHODS).each{|method| instance_variable_set(:"@#{method}", nil) }
|
38
|
+
end
|
39
|
+
|
24
40
|
end
|
25
41
|
|
26
42
|
autoload :Database, 'apartment/database'
|
@@ -33,7 +49,9 @@ module Apartment
|
|
33
49
|
end
|
34
50
|
|
35
51
|
module Elevators
|
36
|
-
autoload :
|
52
|
+
autoload :Generic, 'apartment/elevators/generic'
|
53
|
+
autoload :Subdomain, 'apartment/elevators/subdomain'
|
54
|
+
autoload :Domain, 'apartment/elevators/domain'
|
37
55
|
end
|
38
56
|
|
39
57
|
module Delayed
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module Apartment
|
2
2
|
|
3
3
|
module Database
|
4
|
-
|
4
|
+
|
5
5
|
def self.mysql2_adapter(config)
|
6
|
-
Adapters::
|
6
|
+
Adapters::Mysql2Adapter.new config
|
7
7
|
end
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
module Adapters
|
11
|
-
|
12
|
-
class
|
13
|
-
|
11
|
+
|
12
|
+
class Mysql2Adapter < AbstractAdapter
|
13
|
+
|
14
14
|
protected
|
15
15
|
|
16
16
|
# Connect to new database
|
@@ -26,4 +26,4 @@ module Apartment
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
29
|
-
end
|
29
|
+
end
|
@@ -4,7 +4,7 @@ module Apartment
|
|
4
4
|
|
5
5
|
def self.postgresql_adapter(config)
|
6
6
|
Apartment.use_postgres_schemas ?
|
7
|
-
Adapters::PostgresqlSchemaAdapter.new(config
|
7
|
+
Adapters::PostgresqlSchemaAdapter.new(config) :
|
8
8
|
Adapters::PostgresqlAdapter.new(config)
|
9
9
|
end
|
10
10
|
end
|
@@ -33,27 +33,21 @@ module Apartment
|
|
33
33
|
# Separate Adapter for Postgresql when using schemas
|
34
34
|
class PostgresqlSchemaAdapter < AbstractAdapter
|
35
35
|
|
36
|
-
|
37
|
-
#
|
38
|
-
# @return {String} current schema search path
|
39
|
-
#
|
40
|
-
def current_database
|
41
|
-
ActiveRecord::Base.connection.schema_search_path
|
42
|
-
end
|
36
|
+
attr_reader :current_database
|
43
37
|
|
44
38
|
# Drop the database schema
|
45
39
|
#
|
46
40
|
# @param {String} database Database (schema) to drop
|
47
41
|
#
|
48
42
|
def drop(database)
|
49
|
-
ActiveRecord::Base.connection.execute(
|
43
|
+
ActiveRecord::Base.connection.execute(%{DROP SCHEMA "#{database}" CASCADE})
|
50
44
|
|
51
45
|
rescue ActiveRecord::StatementInvalid
|
52
46
|
raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
|
53
47
|
end
|
54
48
|
|
55
49
|
# Reset search path to default search_path
|
56
|
-
# Set the table_name to always use the
|
50
|
+
# Set the table_name to always use the default namespace for excluded models
|
57
51
|
#
|
58
52
|
def process_excluded_models
|
59
53
|
Apartment.excluded_models.each do |excluded_model|
|
@@ -66,14 +60,14 @@ module Apartment
|
|
66
60
|
|
67
61
|
excluded_model.constantize.tap do |klass|
|
68
62
|
# some models (such as delayed_job) seem to load and cache their column names before this,
|
69
|
-
# so would never get the
|
63
|
+
# so would never get the default prefix, so reset first
|
70
64
|
klass.reset_column_information
|
71
65
|
|
72
66
|
# Ensure that if a schema *was* set, we override
|
73
67
|
table_name = klass.table_name.split('.', 2).last
|
74
68
|
|
75
69
|
# Not sure why, but Delayed::Job somehow ignores table_name_prefix... so we'll just manually set table name instead
|
76
|
-
klass.table_name = "
|
70
|
+
klass.table_name = "#{Apartment.default_schema}.#{table_name}"
|
77
71
|
end
|
78
72
|
end
|
79
73
|
end
|
@@ -83,7 +77,8 @@ module Apartment
|
|
83
77
|
# @return {String} default schema search path
|
84
78
|
#
|
85
79
|
def reset
|
86
|
-
|
80
|
+
@current_database = Apartment.default_schema
|
81
|
+
ActiveRecord::Base.connection.schema_search_path = full_search_path
|
87
82
|
end
|
88
83
|
|
89
84
|
protected
|
@@ -92,7 +87,9 @@ module Apartment
|
|
92
87
|
#
|
93
88
|
def connect_to_new(database = nil)
|
94
89
|
return reset if database.nil?
|
95
|
-
|
90
|
+
|
91
|
+
@current_database = database.to_s
|
92
|
+
ActiveRecord::Base.connection.schema_search_path = full_search_path
|
96
93
|
|
97
94
|
rescue ActiveRecord::StatementInvalid
|
98
95
|
raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
|
@@ -101,12 +98,26 @@ module Apartment
|
|
101
98
|
# Create the new schema
|
102
99
|
#
|
103
100
|
def create_database(database)
|
104
|
-
ActiveRecord::Base.connection.execute(
|
101
|
+
ActiveRecord::Base.connection.execute(%{CREATE SCHEMA "#{database}"})
|
105
102
|
|
106
103
|
rescue ActiveRecord::StatementInvalid
|
107
104
|
raise SchemaExists, "The schema #{database} already exists."
|
108
105
|
end
|
109
106
|
|
107
|
+
private
|
108
|
+
|
109
|
+
# Generate the final search path to set including persistent_schemas
|
110
|
+
#
|
111
|
+
def full_search_path
|
112
|
+
persistent_schemas = persistent_schema_string
|
113
|
+
@current_database.to_s + (persistent_schemas.empty? ? "" : ", #{persistent_schemas}")
|
114
|
+
end
|
115
|
+
|
116
|
+
# Cached persistent schemas joined in a valid search path format (comma separated)
|
117
|
+
#
|
118
|
+
def persistent_schema_string
|
119
|
+
Apartment.persistent_schemas.join(', ')
|
120
|
+
end
|
110
121
|
end
|
111
122
|
end
|
112
123
|
end
|
data/lib/apartment/database.rb
CHANGED
@@ -3,12 +3,15 @@ require 'active_support/core_ext/module/delegation'
|
|
3
3
|
module Apartment
|
4
4
|
|
5
5
|
# The main entry point to Apartment functions
|
6
|
+
#
|
6
7
|
module Database
|
7
8
|
|
8
9
|
extend self
|
9
10
|
|
10
11
|
delegate :create, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch, :to => :adapter
|
11
12
|
|
13
|
+
attr_writer :config
|
14
|
+
|
12
15
|
# Initialize Apartment config options such as excluded_models
|
13
16
|
#
|
14
17
|
def init
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Apartment
|
2
|
+
module Elevators
|
3
|
+
# Provides a rack based db switching solution based on domain
|
4
|
+
# Assumes that database name should match domain
|
5
|
+
# Parses request host for second level domain
|
6
|
+
# eg. example.com => example
|
7
|
+
# www.example.bc.ca => example
|
8
|
+
#
|
9
|
+
class Domain < Generic
|
10
|
+
|
11
|
+
def parse_database_name(request)
|
12
|
+
return nil if request.host.blank?
|
13
|
+
|
14
|
+
request.host.match(/(www.)?(?<sld>[^.]*)/)["sld"]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|