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
data/.pryrc ADDED
@@ -0,0 +1,3 @@
1
+ if defined?(Rails) && Rails.env
2
+ extend Rails::ConsoleMethods
3
+ end
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
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
- In house, we use the subdomain elevator, which analyzes the subdomain of the request and switches
64
- to a database schema of the same name. It can be used like so:
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
- ### Handling Environments
118
+ ### Postgresql Schemas
96
119
 
97
- By default, when not using postgresql schemas, Apartment will prepend the environment to the database name
98
- to ensure there is no conflict between your environments. This is mainly for the benefit of your development
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.prepend_environment = !Rails.env.production?
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
- ### Delayed::Job
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
- def initialize(model)
145
- @model = model
146
- @model.database = Apartment::Database.current_database
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', '~> 1.4.0'
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.8.0'
28
- s.add_development_dependency 'rspec-rails', '~> 2.8.1'
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
- attr_accessor :use_postgres_schemas, :seed_after_create, :prepend_environment
7
- attr_writer :database_names, :excluded_models
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 :Subdomain, 'apartment/elevators/subdomain'
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::MysqlAdapter.new config
6
+ Adapters::Mysql2Adapter.new config
7
7
  end
8
8
  end
9
-
9
+
10
10
  module Adapters
11
-
12
- class MysqlAdapter < AbstractAdapter
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, :schema_search_path => ActiveRecord::Base.connection.schema_search_path) :
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
- # Get the current schema search path
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("DROP SCHEMA \"#{database}\" CASCADE")
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 public namespace for excluded models
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 public prefix, so reset first
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 = "public.#{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
- ActiveRecord::Base.connection.schema_search_path = @defaults[:schema_search_path]
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
- ActiveRecord::Base.connection.schema_search_path = database
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("CREATE SCHEMA \"#{database}\"")
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
@@ -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