apartment 0.23.2 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/HISTORY.md +12 -1
  3. data/README.md +16 -13
  4. data/Rakefile +6 -1
  5. data/TODO.md +9 -14
  6. data/lib/apartment.rb +17 -3
  7. data/lib/apartment/adapters/abstract_adapter.rb +53 -44
  8. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +9 -9
  9. data/lib/apartment/adapters/mysql2_adapter.rb +15 -15
  10. data/lib/apartment/adapters/postgresql_adapter.rb +26 -18
  11. data/lib/apartment/adapters/sqlite3_adapter.rb +13 -13
  12. data/lib/apartment/database.rb +2 -2
  13. data/lib/apartment/elevators/host_hash.rb +3 -3
  14. data/lib/apartment/elevators/subdomain.rb +2 -3
  15. data/lib/apartment/migrator.rb +3 -3
  16. data/lib/apartment/railtie.rb +2 -1
  17. data/lib/apartment/tasks/enhancements.rb +26 -0
  18. data/lib/apartment/version.rb +1 -1
  19. data/lib/generators/apartment/install/templates/apartment.rb +13 -8
  20. data/lib/tasks/apartment.rake +34 -38
  21. data/spec/adapters/jdbc_mysql_adapter_spec.rb +2 -4
  22. data/spec/adapters/jdbc_postgresql_adapter_spec.rb +6 -6
  23. data/spec/adapters/mysql2_adapter_spec.rb +5 -5
  24. data/spec/adapters/postgresql_adapter_spec.rb +6 -6
  25. data/spec/adapters/sqlite3_adapter_spec.rb +2 -2
  26. data/spec/database_spec.rb +2 -2
  27. data/spec/dummy/config/initializers/apartment.rb +2 -2
  28. data/spec/examples/connection_adapter_examples.rb +2 -2
  29. data/spec/examples/generic_adapter_examples.rb +15 -15
  30. data/spec/examples/schema_adapter_examples.rb +6 -6
  31. data/spec/integration/apartment_rake_integration_spec.rb +4 -4
  32. data/spec/integration/query_caching_spec.rb +2 -2
  33. data/spec/support/requirements.rb +2 -5
  34. data/spec/tasks/apartment_rake_spec.rb +8 -9
  35. data/spec/unit/config_spec.rb +17 -10
  36. data/spec/unit/elevators/subdomain_spec.rb +26 -6
  37. metadata +3 -2
@@ -15,37 +15,37 @@ module Apartment
15
15
 
16
16
  protected
17
17
 
18
- # Connect to new database
18
+ # Connect to new tenant
19
19
  # Abstract adapter will catch generic ActiveRecord error
20
20
  # Catch specific adapter errors here
21
21
  #
22
- # @param {String} database Database name
22
+ # @param {String} tenant Tenant name
23
23
  #
24
- def connect_to_new(database = nil)
24
+ def connect_to_new(tenant = nil)
25
25
  super
26
26
  rescue Mysql2::Error
27
27
  Apartment::Database.reset
28
- raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
28
+ raise DatabaseNotFound, "Cannot find tenant #{environmentify(tenant)}"
29
29
  end
30
30
  end
31
31
 
32
32
  class Mysql2SchemaAdapter < AbstractAdapter
33
- attr_reader :default_database
33
+ attr_reader :default_tenant
34
34
 
35
35
  def initialize(config)
36
36
  super
37
37
 
38
- @default_database = config[:database]
38
+ @default_tenant = config[:database]
39
39
  reset
40
40
  end
41
41
 
42
- # Reset current_database to the default_database
42
+ # Reset current_tenant to the default_tenant
43
43
  #
44
44
  def reset
45
- Apartment.connection.execute "use #{default_database}"
45
+ Apartment.connection.execute "use #{default_tenant}"
46
46
  end
47
47
 
48
- # Set the table_name to always use the default database for excluded models
48
+ # Set the table_name to always use the default tenant for excluded models
49
49
  #
50
50
  def process_excluded_models
51
51
  Apartment.excluded_models.each{ |model| process_excluded_model(model) }
@@ -53,16 +53,16 @@ module Apartment
53
53
 
54
54
  protected
55
55
 
56
- # Set schema current_database to new db
56
+ # Set schema current_tenant to new db
57
57
  #
58
- def connect_to_new(database)
59
- return reset if database.nil?
58
+ def connect_to_new(tenant)
59
+ return reset if tenant.nil?
60
60
 
61
- Apartment.connection.execute "use #{environmentify(database)}"
61
+ Apartment.connection.execute "use #{environmentify(tenant)}"
62
62
 
63
63
  rescue ActiveRecord::StatementInvalid
64
64
  Apartment::Database.reset
65
- raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
65
+ raise DatabaseNotFound, "Cannot find tenant #{environmentify(tenant)}"
66
66
  end
67
67
 
68
68
  def process_excluded_model(model)
@@ -74,7 +74,7 @@ module Apartment
74
74
  # Ensure that if a schema *was* set, we override
75
75
  table_name = klass.table_name.split('.', 2).last
76
76
 
77
- klass.table_name = "#{default_database}.#{table_name}"
77
+ klass.table_name = "#{default_tenant}.#{table_name}"
78
78
  end
79
79
  end
80
80
  end
@@ -14,6 +14,14 @@ module Apartment
14
14
  # Default adapter when not using Postgresql Schemas
15
15
  class PostgresqlAdapter < AbstractAdapter
16
16
 
17
+ def drop(tenant)
18
+ # Apartment.connection.drop_database note that drop_database will not throw an exception, so manually execute
19
+ Apartment.connection.execute(%{DROP DATABASE "#{tenant}"})
20
+
21
+ rescue *rescuable_exceptions
22
+ raise DatabaseNotFound, "The tenant #{tenant} cannot be found"
23
+ end
24
+
17
25
  private
18
26
 
19
27
  def rescue_from
@@ -30,15 +38,15 @@ module Apartment
30
38
  reset
31
39
  end
32
40
 
33
- # Drop the database schema
41
+ # Drop the tenant
34
42
  #
35
- # @param {String} database Database (schema) to drop
43
+ # @param {String} tenant Database (schema) to drop
36
44
  #
37
- def drop(database)
38
- Apartment.connection.execute(%{DROP SCHEMA "#{database}" CASCADE})
45
+ def drop(tenant)
46
+ Apartment.connection.execute(%{DROP SCHEMA "#{tenant}" CASCADE})
39
47
 
40
48
  rescue *rescuable_exceptions
41
- raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
49
+ raise SchemaNotFound, "The schema #{tenant.inspect} cannot be found."
42
50
  end
43
51
 
44
52
  # Reset search path to default search_path
@@ -64,36 +72,36 @@ module Apartment
64
72
  # @return {String} default schema search path
65
73
  #
66
74
  def reset
67
- @current_database = Apartment.default_schema
75
+ @current_tenant = Apartment.default_schema
68
76
  Apartment.connection.schema_search_path = full_search_path
69
77
  end
70
78
 
71
- def current_database
72
- @current_database || Apartment.default_schema
79
+ def current_tenant
80
+ @current_tenant || Apartment.default_schema
73
81
  end
74
82
 
75
83
  protected
76
84
 
77
85
  # Set schema search path to new schema
78
86
  #
79
- def connect_to_new(database = nil)
80
- return reset if database.nil?
81
- raise ActiveRecord::StatementInvalid.new("Could not find schema #{database}") unless Apartment.connection.schema_exists? database
87
+ def connect_to_new(tenant = nil)
88
+ return reset if tenant.nil?
89
+ raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.schema_exists? tenant
82
90
 
83
- @current_database = database.to_s
91
+ @current_tenant = tenant.to_s
84
92
  Apartment.connection.schema_search_path = full_search_path
85
93
 
86
94
  rescue *rescuable_exceptions
87
- raise SchemaNotFound, "One of the following schema(s) is invalid: #{database}, #{full_search_path}"
95
+ raise SchemaNotFound, "One of the following schema(s) is invalid: #{tenant}, #{full_search_path}"
88
96
  end
89
97
 
90
98
  # Create the new schema
91
99
  #
92
- def create_tenant(database)
93
- Apartment.connection.execute(%{CREATE SCHEMA "#{database}"})
100
+ def create_tenant(tenant)
101
+ Apartment.connection.execute(%{CREATE SCHEMA "#{tenant}"})
94
102
 
95
103
  rescue *rescuable_exceptions
96
- raise SchemaExists, "The schema #{database} already exists."
104
+ raise SchemaExists, "The schema #{tenant} already exists."
97
105
  end
98
106
 
99
107
  private
@@ -103,9 +111,9 @@ module Apartment
103
111
  def full_search_path
104
112
  persistent_schemas.map(&:inspect).join(", ")
105
113
  end
106
-
114
+
107
115
  def persistent_schemas
108
- [@current_database, Apartment.persistent_schemas].flatten
116
+ [@current_tenant, Apartment.persistent_schemas].flatten
109
117
  end
110
118
  end
111
119
  end
@@ -15,38 +15,38 @@ module Apartment
15
15
  super
16
16
  end
17
17
 
18
- def drop(database)
18
+ def drop(tenant)
19
19
  raise DatabaseNotFound,
20
- "The database #{environmentify(database)} cannot be found." unless File.exists?(database_file(database))
20
+ "The tenant #{environmentify(tenant)} cannot be found." unless File.exists?(database_file(tenant))
21
21
 
22
- File.delete(database_file(database))
22
+ File.delete(database_file(tenant))
23
23
  end
24
24
 
25
- def current_database
25
+ def current_tenant
26
26
  File.basename(Apartment.connection.instance_variable_get(:@config)[:database], '.sqlite3')
27
27
  end
28
28
 
29
29
  protected
30
30
 
31
- def connect_to_new(database)
31
+ def connect_to_new(tenant)
32
32
  raise DatabaseNotFound,
33
- "The database #{environmentify(database)} cannot be found." unless File.exists?(database_file(database))
33
+ "The tenant #{environmentify(tenant)} cannot be found." unless File.exists?(database_file(tenant))
34
34
 
35
- super database_file(database)
35
+ super database_file(tenant)
36
36
  end
37
37
 
38
- def create_tenant(database)
38
+ def create_tenant(tenant)
39
39
  raise DatabaseExists,
40
- "The database #{environmentify(database)} already exists." if File.exists?(database_file(database))
40
+ "The tenant #{environmentify(tenant)} already exists." if File.exists?(database_file(tenant))
41
41
 
42
- f = File.new(database_file(database), File::CREAT)
42
+ f = File.new(database_file(tenant), File::CREAT)
43
43
  f.close
44
44
  end
45
45
 
46
- private
46
+ private
47
47
 
48
- def database_file(database)
49
- "#{@default_dir}/#{database}.sqlite3"
48
+ def database_file(tenant)
49
+ "#{@default_dir}/#{tenant}.sqlite3"
50
50
  end
51
51
  end
52
52
  end
@@ -8,7 +8,7 @@ module Apartment
8
8
  extend self
9
9
  extend Forwardable
10
10
 
11
- def_delegators :adapter, :create, :current_database, :current, :drop, :process, :process_excluded_models, :reset, :seed, :switch
11
+ def_delegators :adapter, :create, :current_tenant, :current, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch
12
12
 
13
13
  attr_writer :config
14
14
 
@@ -63,4 +63,4 @@ module Apartment
63
63
  @config ||= Rails.configuration.database_configuration[Rails.env].symbolize_keys
64
64
  end
65
65
  end
66
- end
66
+ end
@@ -2,8 +2,8 @@ require 'apartment/elevators/generic'
2
2
 
3
3
  module Apartment
4
4
  module Elevators
5
- # Provides a rack based db switching solution based on hosts
6
- # Uses a hash to find the corresponding database name for the host
5
+ # Provides a rack based tenant switching solution based on hosts
6
+ # Uses a hash to find the corresponding tenant name for the host
7
7
  #
8
8
  class HostHash < Generic
9
9
  def initialize(app, hash = {}, processor = nil)
@@ -13,7 +13,7 @@ module Apartment
13
13
 
14
14
  def parse_tenant_name(request)
15
15
  raise DatabaseNotFound,
16
- "Cannot find database for host #{request.host}" unless @hash.has_key?(request.host)
16
+ "Cannot find tenant for host #{request.host}" unless @hash.has_key?(request.host)
17
17
 
18
18
  @hash[request.host]
19
19
  end
@@ -37,11 +37,10 @@ module Apartment
37
37
  subdomains(host).first
38
38
  end
39
39
 
40
- # Assuming tld_length of 1, might need to make this configurable in Apartment in the future for things like .co.uk
41
- def subdomains(host, tld_length = 1)
40
+ def subdomains(host)
42
41
  return [] unless named_host?(host)
43
42
 
44
- host.split('.')[0..-(tld_length + 2)]
43
+ host.split('.')[0..-(Apartment.tld_length + 2)]
45
44
  end
46
45
 
47
46
  def named_host?(host)
@@ -10,7 +10,7 @@ module Apartment
10
10
  Database.process(database) do
11
11
  version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
12
12
 
13
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_path, version) do |migration|
13
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version) do |migration|
14
14
  ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
15
15
  end
16
16
  end
@@ -19,14 +19,14 @@ module Apartment
19
19
  # Migrate up/down to a specific version
20
20
  def run(direction, database, version)
21
21
  Database.process(database) do
22
- ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_path, version)
22
+ ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_paths, version)
23
23
  end
24
24
  end
25
25
 
26
26
  # rollback latest migration `step` number of times
27
27
  def rollback(database, step = 1)
28
28
  Database.process(database) do
29
- ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_path, step)
29
+ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
30
30
  end
31
31
  end
32
32
  end
@@ -13,10 +13,11 @@ module Apartment
13
13
  Apartment.configure do |config|
14
14
  config.excluded_models = []
15
15
  config.use_schemas = true
16
- config.database_names = []
16
+ config.tenant_names = []
17
17
  config.seed_after_create = false
18
18
  config.prepend_environment = false
19
19
  config.append_environment = false
20
+ config.tld_length = 1
20
21
  end
21
22
 
22
23
  ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
@@ -0,0 +1,26 @@
1
+ # Require this file to append Apartment rake tasks to ActiveRecord db rake tasks
2
+ # Enabled by default in the initializer
3
+
4
+ Rake::Task["db:migrate"].enhance do
5
+ Rake::Task["apartment:migrate"].invoke
6
+ end
7
+
8
+ Rake::Task["db:rollback"].enhance do
9
+ Rake::Task["apartment:rollback"].invoke
10
+ end
11
+
12
+ Rake::Task["db:migrate:up"].enhance do
13
+ Rake::Task["apartment:migrate:up"].invoke
14
+ end
15
+
16
+ Rake::Task["db:migrate:down"].enhance do
17
+ Rake::Task["apartment:migrate:down"].invoke
18
+ end
19
+
20
+ Rake::Task["db:migrate:redo"].enhance do
21
+ Rake::Task["apartment:migrate:redo"].invoke
22
+ end
23
+
24
+ Rake::Task["db:seed"].enhance do
25
+ Rake::Task["apartment:seed"].invoke
26
+ end
@@ -1,3 +1,3 @@
1
1
  module Apartment
2
- VERSION = "0.23.2"
2
+ VERSION = "0.24.0"
3
3
  end
@@ -9,13 +9,15 @@ require 'apartment/elevators/subdomain'
9
9
  #
10
10
  Apartment.configure do |config|
11
11
 
12
- # these models will not be multi-tenanted,
12
+ # These models will not be multi-tenanted,
13
13
  # but remain in the global (public) namespace
14
- # Note that ActiveRecord::SessionStore::Session is just an example
15
- # you may not even use the AR Session Store, in which case you'd remove that line
16
- config.excluded_models = %w{
17
- ActiveRecord::SessionStore::Session
18
- }
14
+ #
15
+ # An example might be a Customer or Tenant model that stores each tenant information
16
+ # ex:
17
+ #
18
+ # config.excluded_models = %w{Tenant}
19
+ #
20
+ config.excluded_models = %w{}
19
21
 
20
22
  # use postgres schemas?
21
23
  config.use_schemas = true
@@ -28,8 +30,7 @@ Apartment.configure do |config|
28
30
  # config.append_environment = true
29
31
 
30
32
  # supply list of database names for migrations to run on
31
- config.database_names = lambda{ ToDo_Tenant_Or_User_Model.pluck :database }
32
-
33
+ config.tenant_names = lambda{ ToDo_Tenant_Or_User_Model.pluck :database }
33
34
  end
34
35
 
35
36
  ##
@@ -42,3 +43,7 @@ end
42
43
  # Rails.application.config.middleware.use 'Apartment::Elevators::Domain'
43
44
 
44
45
  Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'
46
+
47
+ ##
48
+ # Rake enhancements so that db:migrate etc... also runs migrations on all tenants
49
+ require 'apartment/tasks/enhancements'
@@ -2,38 +2,36 @@ require 'apartment/migrator'
2
2
 
3
3
  apartment_namespace = namespace :apartment do
4
4
 
5
- desc "Create all multi-tenant databases"
6
- task :create => 'db:migrate' do
7
- database_names.each do |db|
5
+ desc "Create all tenants"
6
+ task create: 'db:migrate' do
7
+ tenants.each do |tenant|
8
8
  begin
9
- puts("Creating #{db} database")
10
- quietly { Apartment::Database.create(db) }
9
+ puts("Creating #{tenant} tenant")
10
+ quietly { Apartment::Database.create(tenant) }
11
11
  rescue Apartment::TenantExists => e
12
12
  puts e.message
13
13
  end
14
14
  end
15
15
  end
16
16
 
17
- desc "Migrate all multi-tenant databases"
18
- task :migrate => 'db:migrate' do
19
-
20
- database_names.each do |db|
17
+ desc "Migrate all tenants"
18
+ task :migrate do
19
+ tenants.each do |tenant|
21
20
  begin
22
- puts("Migrating #{db} database")
23
- Apartment::Migrator.migrate db
21
+ puts("Migrating #{tenant} tenant")
22
+ Apartment::Migrator.migrate tenant
24
23
  rescue Apartment::TenantNotFound => e
25
24
  puts e.message
26
25
  end
27
26
  end
28
27
  end
29
28
 
30
- desc "Seed all multi-tenant databases"
31
- task :seed => 'db:seed' do
32
-
33
- database_names.each do |db|
29
+ desc "Seed all tenants"
30
+ task :seed do
31
+ tenants.each do |tenant|
34
32
  begin
35
- puts("Seeding #{db} database")
36
- Apartment::Database.process(db) do
33
+ puts("Seeding #{tenant} tenant")
34
+ Apartment::Database.process(tenant) do
37
35
  Apartment::Database.seed
38
36
  end
39
37
  rescue Apartment::TenantNotFound => e
@@ -42,14 +40,14 @@ apartment_namespace = namespace :apartment do
42
40
  end
43
41
  end
44
42
 
45
- desc "Rolls the schema back to the previous version (specify steps w/ STEP=n) across all multi-tenant dbs."
46
- task :rollback => 'db:rollback' do
43
+ desc "Rolls the migration back to the previous version (specify steps w/ STEP=n) across all tenants."
44
+ task :rollback do
47
45
  step = ENV['STEP'] ? ENV['STEP'].to_i : 1
48
46
 
49
- database_names.each do |db|
47
+ tenants.each do |tenant|
50
48
  begin
51
- puts("Rolling back #{db} database")
52
- Apartment::Migrator.rollback db, step
49
+ puts("Rolling back #{tenant} tenant")
50
+ Apartment::Migrator.rollback tenant, step
53
51
  rescue Apartment::TenantNotFound => e
54
52
  puts e.message
55
53
  end
@@ -57,39 +55,38 @@ apartment_namespace = namespace :apartment do
57
55
  end
58
56
 
59
57
  namespace :migrate do
60
-
61
- desc 'Runs the "up" for a given migration VERSION across all multi-tenant dbs.'
62
- task :up => 'db:migrate:up' do
58
+ desc 'Runs the "up" for a given migration VERSION across all tenants.'
59
+ task :up do
63
60
  version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
64
61
  raise 'VERSION is required' unless version
65
62
 
66
- database_names.each do |db|
63
+ tenants.each do |tenant|
67
64
  begin
68
- puts("Migrating #{db} database up")
69
- Apartment::Migrator.run :up, db, version
65
+ puts("Migrating #{tenant} tenant up")
66
+ Apartment::Migrator.run :up, tenant, version
70
67
  rescue Apartment::TenantNotFound => e
71
68
  puts e.message
72
69
  end
73
70
  end
74
71
  end
75
72
 
76
- desc 'Runs the "down" for a given migration VERSION across all multi-tenant dbs.'
77
- task :down => 'db:migrate:down' do
73
+ desc 'Runs the "down" for a given migration VERSION across all tenants.'
74
+ task :down do
78
75
  version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
79
76
  raise 'VERSION is required' unless version
80
77
 
81
- database_names.each do |db|
78
+ tenants.each do |tenant|
82
79
  begin
83
- puts("Migrating #{db} database down")
84
- Apartment::Migrator.run :down, db, version
80
+ puts("Migrating #{tenant} tenant down")
81
+ Apartment::Migrator.run :down, tenant, version
85
82
  rescue Apartment::TenantNotFound => e
86
83
  puts e.message
87
84
  end
88
85
  end
89
86
  end
90
87
 
91
- desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
92
- task :redo => 'db:migrate:redo' do
88
+ desc 'Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).'
89
+ task :redo do
93
90
  if ENV['VERSION']
94
91
  apartment_namespace['migrate:down'].invoke
95
92
  apartment_namespace['migrate:up'].invoke
@@ -98,10 +95,9 @@ apartment_namespace = namespace :apartment do
98
95
  apartment_namespace['migrate'].invoke
99
96
  end
100
97
  end
101
-
102
98
  end
103
99
 
104
- def database_names
105
- ENV['DB'] ? ENV['DB'].split(',').map { |s| s.strip } : Apartment.database_names
100
+ def tenants
101
+ ENV['DB'] ? ENV['DB'].split(',').map { |s| s.strip } : Apartment.tenant_names
106
102
  end
107
103
  end