puzzle-apartment 2.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +71 -0
  3. data/.github/ISSUE_TEMPLATE.md +21 -0
  4. data/.github/workflows/changelog.yml +63 -0
  5. data/.github/workflows/reviewdog.yml +22 -0
  6. data/.gitignore +15 -0
  7. data/.pryrc +5 -0
  8. data/.rspec +4 -0
  9. data/.rubocop.yml +33 -0
  10. data/.rubocop_todo.yml +418 -0
  11. data/.ruby-version +1 -0
  12. data/.story_branch.yml +5 -0
  13. data/Appraisals +49 -0
  14. data/CHANGELOG.md +963 -0
  15. data/Gemfile +17 -0
  16. data/Guardfile +11 -0
  17. data/HISTORY.md +496 -0
  18. data/README.md +652 -0
  19. data/Rakefile +157 -0
  20. data/TODO.md +50 -0
  21. data/docker-compose.yml +33 -0
  22. data/gemfiles/rails_6_1.gemfile +17 -0
  23. data/gemfiles/rails_7_0.gemfile +17 -0
  24. data/gemfiles/rails_7_1.gemfile +17 -0
  25. data/gemfiles/rails_master.gemfile +17 -0
  26. data/lib/apartment/active_record/connection_handling.rb +34 -0
  27. data/lib/apartment/active_record/internal_metadata.rb +9 -0
  28. data/lib/apartment/active_record/postgresql_adapter.rb +39 -0
  29. data/lib/apartment/active_record/schema_migration.rb +13 -0
  30. data/lib/apartment/adapters/abstract_adapter.rb +275 -0
  31. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +20 -0
  32. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +19 -0
  33. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +62 -0
  34. data/lib/apartment/adapters/mysql2_adapter.rb +77 -0
  35. data/lib/apartment/adapters/postgis_adapter.rb +13 -0
  36. data/lib/apartment/adapters/postgresql_adapter.rb +284 -0
  37. data/lib/apartment/adapters/sqlite3_adapter.rb +66 -0
  38. data/lib/apartment/console.rb +24 -0
  39. data/lib/apartment/custom_console.rb +42 -0
  40. data/lib/apartment/deprecation.rb +11 -0
  41. data/lib/apartment/elevators/domain.rb +23 -0
  42. data/lib/apartment/elevators/first_subdomain.rb +18 -0
  43. data/lib/apartment/elevators/generic.rb +33 -0
  44. data/lib/apartment/elevators/host.rb +35 -0
  45. data/lib/apartment/elevators/host_hash.rb +26 -0
  46. data/lib/apartment/elevators/subdomain.rb +66 -0
  47. data/lib/apartment/log_subscriber.rb +45 -0
  48. data/lib/apartment/migrator.rb +52 -0
  49. data/lib/apartment/model.rb +29 -0
  50. data/lib/apartment/railtie.rb +68 -0
  51. data/lib/apartment/tasks/enhancements.rb +55 -0
  52. data/lib/apartment/tasks/task_helper.rb +52 -0
  53. data/lib/apartment/tenant.rb +63 -0
  54. data/lib/apartment/version.rb +5 -0
  55. data/lib/apartment.rb +159 -0
  56. data/lib/generators/apartment/install/USAGE +5 -0
  57. data/lib/generators/apartment/install/install_generator.rb +11 -0
  58. data/lib/generators/apartment/install/templates/apartment.rb +116 -0
  59. data/lib/tasks/apartment.rake +106 -0
  60. data/puzzle-apartment.gemspec +59 -0
  61. metadata +385 -0
data/Rakefile ADDED
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler'
5
+ rescue StandardError
6
+ 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+ Bundler.setup
9
+ Bundler::GemHelper.install_tasks
10
+
11
+ require 'appraisal'
12
+
13
+ require 'rspec'
14
+ require 'rspec/core/rake_task'
15
+
16
+ RSpec::Core::RakeTask.new(spec: %w[db:copy_credentials db:test:prepare]) do |spec|
17
+ spec.pattern = 'spec/**/*_spec.rb'
18
+ # spec.rspec_opts = '--order rand:47078'
19
+ end
20
+
21
+ namespace :spec do
22
+ %i[tasks unit adapters integration].each do |type|
23
+ RSpec::Core::RakeTask.new(type => :spec) do |spec|
24
+ spec.pattern = "spec/#{type}/**/*_spec.rb"
25
+ end
26
+ end
27
+ end
28
+
29
+ task :console do
30
+ require 'pry'
31
+ require 'apartment'
32
+ ARGV.clear
33
+ Pry.start
34
+ end
35
+
36
+ task default: :spec
37
+
38
+ namespace :db do
39
+ namespace :test do
40
+ task prepare: %w[postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db]
41
+ end
42
+
43
+ desc "copy sample database credential files over if real files don't exist"
44
+ task :copy_credentials do
45
+ require 'fileutils'
46
+ apartment_db_file = 'spec/config/database.yml'
47
+ rails_db_file = 'spec/dummy/config/database.yml'
48
+
49
+ unless File.exist?(apartment_db_file)
50
+ FileUtils.copy("#{apartment_db_file}.sample", apartment_db_file, verbose: true)
51
+ end
52
+ FileUtils.copy("#{rails_db_file}.sample", rails_db_file, verbose: true) unless File.exist?(rails_db_file)
53
+ end
54
+ end
55
+
56
+ namespace :postgres do
57
+ require 'active_record'
58
+ require File.join(File.dirname(__FILE__), 'spec', 'support', 'config').to_s
59
+
60
+ desc 'Build the PostgreSQL test databases'
61
+ task :build_db do
62
+ params = []
63
+ params << '-E UTF8'
64
+ params << pg_config['database']
65
+ params << "-U#{pg_config['username']}"
66
+ params << "-h#{pg_config['host']}" if pg_config['host']
67
+ params << "-p#{pg_config['port']}" if pg_config['port']
68
+
69
+ begin
70
+ `createdb #{params.join(' ')}`
71
+ rescue StandardError
72
+ 'test db already exists'
73
+ end
74
+ ActiveRecord::Base.establish_connection pg_config
75
+ migrate
76
+ end
77
+
78
+ desc 'drop the PostgreSQL test database'
79
+ task :drop_db do
80
+ puts "dropping database #{pg_config['database']}"
81
+ params = []
82
+ params << pg_config['database']
83
+ params << "-U#{pg_config['username']}"
84
+ params << "-h#{pg_config['host']}" if pg_config['host']
85
+ params << "-p#{pg_config['port']}" if pg_config['port']
86
+ `dropdb #{params.join(' ')}`
87
+ end
88
+ end
89
+
90
+ namespace :mysql do
91
+ require 'active_record'
92
+ require File.join(File.dirname(__FILE__), 'spec', 'support', 'config').to_s
93
+
94
+ desc 'Build the MySQL test databases'
95
+ task :build_db do
96
+ params = []
97
+ params << "-h #{my_config['host']}" if my_config['host']
98
+ params << "-u #{my_config['username']}" if my_config['username']
99
+ params << "-p#{my_config['password']}" if my_config['password']
100
+ params << "--port #{my_config['port']}" if my_config['port']
101
+ begin
102
+ `mysqladmin #{params.join(' ')} create #{my_config['database']}`
103
+ rescue StandardError
104
+ 'test db already exists'
105
+ end
106
+ ActiveRecord::Base.establish_connection my_config
107
+ migrate
108
+ end
109
+
110
+ desc 'drop the MySQL test database'
111
+ task :drop_db do
112
+ puts "dropping database #{my_config['database']}"
113
+ params = []
114
+ params << "-h #{my_config['host']}" if my_config['host']
115
+ params << "-u #{my_config['username']}" if my_config['username']
116
+ params << "-p#{my_config['password']}" if my_config['password']
117
+ params << "--port #{my_config['port']}" if my_config['port']
118
+ `mysqladmin #{params.join(' ')} drop #{my_config['database']} --force`
119
+ end
120
+ end
121
+
122
+ # TODO: clean this up
123
+ def config
124
+ Apartment::Test.config['connections']
125
+ end
126
+
127
+ def pg_config
128
+ config['postgresql']
129
+ end
130
+
131
+ def my_config
132
+ config['mysql']
133
+ end
134
+
135
+ def activerecord_below_5_2?
136
+ ActiveRecord.version.release < Gem::Version.new('5.2.0')
137
+ end
138
+
139
+ def activerecord_below_6_0?
140
+ ActiveRecord.version.release < Gem::Version.new('6.0.0')
141
+ end
142
+
143
+ def activerecord_above_7_0?
144
+ ActiveRecord.version.release > Gem::Version.new('7.0.0')
145
+ end
146
+
147
+ def migrate
148
+ if activerecord_below_5_2?
149
+ ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
150
+ elsif activerecord_below_6_0? || activerecord_above_7_0?
151
+ ActiveRecord::MigrationContext.new('spec/dummy/db/migrate').migrate
152
+ else
153
+ # TODO: Figure out if there is any other possibility that can/should be
154
+ # passed here as the second argument for the migration context
155
+ ActiveRecord::MigrationContext.new('spec/dummy/db/migrate', ActiveRecord::SchemaMigration).migrate
156
+ end
157
+ end
data/TODO.md ADDED
@@ -0,0 +1,50 @@
1
+ # Apartment TODOs
2
+
3
+ ### Below is a list of tasks in the approximate order to be completed of for Apartment
4
+ ### Any help along the way is greatly appreciated (on any items, not particularly in order)
5
+
6
+ 1. Apartment was originally written (and TDD'd) with just Postgresql in mind. Different adapters were added at a later date.
7
+ As such, the test suite is a bit of a mess. There's no formal structure for fully integration testing all adapters to ensure
8
+ proper quality and prevent regressions.
9
+
10
+ There's also a test order dependency as some tests run assuming a db connection and if that test randomly ran before a previous
11
+ one that makes the connection, it would fail.
12
+
13
+ I'm proposing the first thing to be done is to write up a standard, high livel integration test case that can be applied to all adapters
14
+ and makes no assumptions about implementation. It should ensure that each adapter conforms to the Apartment Interface and CRUD's properly.
15
+ It would be nice if a user can 'register' an adapter such that it would automatically be tested (nice to have). Otherwise one could just use
16
+ a shared behaviour to run through all of this.
17
+
18
+ Then, I'd like to see all of the implementation specific tests just in their own test file for each adapter (ie the postgresql schema adapter checks a lot of things with `schema_search_path`)
19
+
20
+ This should ensure that going forward nothing breaks, and we should *ideally* be able to randomize the test order
21
+
22
+ 2. <del>`Apartment::Database` is the wrong abstraction. When dealing with a multi-tenanted system, users shouldn't thing about 'Databases', they should
23
+ think about Tenants. I proprose that we deprecate the `Apartment::Database` constant in favour of `Apartment::Tenant` for a nicer abstraction. See
24
+ http://myronmars.to/n/dev-blog/2011/09/deprecating-constants-and-classes-in-ruby for ideas on how to achieve this.</del>
25
+
26
+ 4. Apartment::Database.process should be deprecated in favour of just passing a block to `switch`
27
+ 5. Apartment::Database.switch should be renamed to switch! to indicate that using it on its own has side effects
28
+
29
+ 6. Migrations right now can be a bit of a pain. Apartment currently migrates a single tenant completely up to date, then goes onto the next. If one of these
30
+ migrations fails on a tenant, the previous one does NOT get reverted and leaves you in an awkward state. Ideally we'd want to wrap all of the migrations in
31
+ a transaction so if one fails, the whole thing reverts. Once we can ensure an all-or-nothing approach to migrations, we can optimize the migration strategy
32
+ to not even iterate over the tenants if there are no migrations to run on public.
33
+
34
+ 7. Apartment has be come one of the most popular/robust Multi-tenant gems for Rails, but it still doesn't work for everyone's use case. It's fairly limited in implementation to either schema based (ie postgresql schemas) or connection based. I'd like to abstract out these implementation details such that one could write a pluggable strategy for Apartment and choose it based on a config selection (something like `config.strategy = :schema`). The next implementation I'd like to see is a scoped based approach that uses a `tenant_id` scoping on all records for multi-tenancy. This is probably the most popular multi-tenant approach and is db independent and really the simplest mechanism for a type of multi-tenancy.
35
+
36
+ 8. Right now excluded tables still live in all tenanted environments. This is basically because it doesn't matter if they're there, we always query from the public.
37
+ It's a bit of an annoyance though and confuses lots of people. I'd love to see only tenanted tables in the tenants and only excluded tables in the public tenant.
38
+ This will be hard because Rails uses public to generate schema.rb. One idea is to have an `excluded` schema that holds all the excluded models and the public can
39
+ maintain everything.
40
+
41
+ 9. This one is pretty lofty, but I'd also like to abstract out the fact that Apartment uses ActiveRecord. With the new DataMapper coming out soon and other popular
42
+ DBMS's (ie. mongo, couch etc...), it'd be nice if Apartment could be the de-facto interface for multi-tenancy on these systems.
43
+
44
+
45
+ ===================
46
+
47
+ Quick TODOs
48
+
49
+ 2. deprecation.rb rescues everything, we have a hard dependency on ActiveSupport so this is unnecessary
50
+ 3.
@@ -0,0 +1,33 @@
1
+ version: '2.3'
2
+ services:
3
+ postgresql:
4
+ image: postgres:9.5.12
5
+ environment:
6
+ POSTGRES_PASSWORD: ""
7
+ ports:
8
+ - "5432:5432"
9
+ healthcheck:
10
+ test: pg_isready -U postgres
11
+ start_period: 10s
12
+ interval: 10s
13
+ timeout: 30s
14
+ retries: 3
15
+ mysql:
16
+ image: mysql:5.7
17
+ environment:
18
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
19
+ ports:
20
+ - "3306:3306"
21
+ healthcheck:
22
+ test: mysqladmin -h 127.0.0.1 -uroot ping
23
+ start_period: 15s
24
+ interval: 10s
25
+ timeout: 30s
26
+ retries: 3
27
+ healthcheck:
28
+ image: busybox
29
+ depends_on:
30
+ postgresql:
31
+ condition: service_healthy
32
+ mysql:
33
+ condition: service_healthy
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 6.1.0"
6
+
7
+ platforms :ruby do
8
+ gem "sqlite3", "~> 1.4"
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem "activerecord-jdbc-adapter", "~> 61.0"
13
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0"
14
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0"
15
+ end
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 7.0.0"
6
+
7
+ platforms :ruby do
8
+ gem "sqlite3", "~> 1.4"
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem "activerecord-jdbc-adapter", "~> 61.0"
13
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0"
14
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0"
15
+ end
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 7.1.0"
6
+
7
+ platforms :ruby do
8
+ gem "sqlite3", "~> 1.6"
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem "activerecord-jdbc-adapter", "~> 61.0"
13
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0"
14
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0"
15
+ end
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", git: "https://github.com/rails/rails.git"
6
+
7
+ platforms :ruby do
8
+ gem "sqlite3", "~> 1.4"
9
+ end
10
+
11
+ platforms :jruby do
12
+ gem "activerecord-jdbc-adapter", "~> 61.0"
13
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0"
14
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0"
15
+ end
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord # :nodoc:
4
+ # This is monkeypatching Active Record to ensure that whenever a new connection is established it
5
+ # switches to the same tenant as before the connection switching. This problem is more evident when
6
+ # using read replica in Rails 6
7
+ module ConnectionHandling
8
+ if ActiveRecord::VERSION::MAJOR >= 6 && ActiveRecord::VERSION::MAJOR < 7.1
9
+ def connected_to_with_tenant(role: nil, prevent_writes: false, &blk)
10
+ current_tenant = Apartment::Tenant.current
11
+
12
+ connected_to_without_tenant(role: role, prevent_writes: prevent_writes) do
13
+ Apartment::Tenant.switch!(current_tenant)
14
+ yield(blk)
15
+ end
16
+ end
17
+
18
+ alias connected_to_without_tenant connected_to
19
+ alias connected_to connected_to_with_tenant
20
+ elsif ActiveRecord::VERSION::MAJOR >= 7.1
21
+ def connected_to_with_tenant(role: nil, shard: nil, prevent_writes: false, &blk)
22
+ current_tenant = Apartment::Tenant.current
23
+
24
+ connected_to_without_tenant(role: role, shard: shard, prevent_writes: prevent_writes) do
25
+ Apartment::Tenant.switch!(current_tenant)
26
+ yield(blk)
27
+ end
28
+ end
29
+
30
+ alias connected_to_without_tenant connected_to
31
+ alias connected_to connected_to_with_tenant
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class InternalMetadata < ActiveRecord::Base # :nodoc:
4
+ class << self
5
+ def table_exists?
6
+ connection.table_exists?(table_name)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/ClassAndModuleChildren
4
+
5
+ # NOTE: This patch is meant to remove any schema_prefix appart from the ones for
6
+ # excluded models. The schema_prefix would be resolved by apartment's setting
7
+ # of search path
8
+ module Apartment::PostgreSqlAdapterPatch
9
+ def default_sequence_name(table, _column)
10
+ res = super
11
+ schema_prefix = "#{Apartment::Tenant.current}."
12
+ default_tenant_prefix = "#{Apartment::Tenant.default_tenant}."
13
+
14
+ # NOTE: Excluded models should always access the sequence from the default
15
+ # tenant schema
16
+ if excluded_model?(table)
17
+ res.sub!(schema_prefix, default_tenant_prefix) if schema_prefix != default_tenant_prefix
18
+ return res
19
+ end
20
+
21
+ res.delete_prefix!(schema_prefix) if res&.starts_with?(schema_prefix)
22
+
23
+ res
24
+ end
25
+
26
+ private
27
+
28
+ def excluded_model?(table)
29
+ Apartment.excluded_models.any? { |m| m.constantize.table_name == table }
30
+ end
31
+ end
32
+
33
+ require 'active_record/connection_adapters/postgresql_adapter'
34
+
35
+ # NOTE: inject this into postgresql adapters
36
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
37
+ include Apartment::PostgreSqlAdapterPatch
38
+ end
39
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/schema_migration'
4
+
5
+ module ActiveRecord
6
+ class SchemaMigration # :nodoc:
7
+ class << self
8
+ def table_exists?
9
+ connection.table_exists?(table_name)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apartment
4
+ module Adapters
5
+ # Abstract adapter from which all the Apartment DB related adapters will inherit the base logic
6
+ class AbstractAdapter
7
+ include ActiveSupport::Callbacks
8
+ define_callbacks :create, :switch
9
+
10
+ attr_writer :default_tenant
11
+
12
+ # @constructor
13
+ # @param {Hash} config Database config
14
+ #
15
+ def initialize(config)
16
+ @config = config
17
+ end
18
+
19
+ # Create a new tenant, import schema, seed if appropriate
20
+ #
21
+ # @param {String} tenant Tenant name
22
+ #
23
+ def create(tenant)
24
+ run_callbacks :create do
25
+ create_tenant(tenant)
26
+
27
+ switch(tenant) do
28
+ import_database_schema
29
+
30
+ # Seed data if appropriate
31
+ seed_data if Apartment.seed_after_create
32
+
33
+ yield if block_given?
34
+ end
35
+ end
36
+ end
37
+
38
+ # Initialize Apartment config options such as excluded_models
39
+ #
40
+ def init
41
+ process_excluded_models
42
+ end
43
+
44
+ # Note alias_method here doesn't work with inheritence apparently ??
45
+ #
46
+ def current
47
+ Apartment.connection.current_database
48
+ end
49
+
50
+ # Return the original public tenant
51
+ #
52
+ # @return {String} default tenant name
53
+ #
54
+ def default_tenant
55
+ @default_tenant || Apartment.default_tenant
56
+ end
57
+
58
+ # Drop the tenant
59
+ #
60
+ # @param {String} tenant name
61
+ #
62
+ def drop(tenant)
63
+ with_neutral_connection(tenant) do |conn|
64
+ drop_command(conn, tenant)
65
+ end
66
+ rescue *rescuable_exceptions => e
67
+ raise_drop_tenant_error!(tenant, e)
68
+ end
69
+
70
+ # Switch to a new tenant
71
+ #
72
+ # @param {String} tenant name
73
+ #
74
+ def switch!(tenant = nil)
75
+ run_callbacks :switch do
76
+ connect_to_new(tenant).tap do
77
+ Apartment.connection.clear_query_cache
78
+ end
79
+ end
80
+ end
81
+
82
+ # Connect to tenant, do your biz, switch back to previous tenant
83
+ #
84
+ # @param {String?} tenant to connect to
85
+ #
86
+ def switch(tenant = nil)
87
+ previous_tenant = current
88
+ switch!(tenant)
89
+ yield
90
+ ensure
91
+ begin
92
+ switch!(previous_tenant)
93
+ rescue StandardError => _e
94
+ reset
95
+ end
96
+ end
97
+
98
+ # Iterate over all tenants, switch to tenant and yield tenant name
99
+ #
100
+ def each(tenants = Apartment.tenant_names)
101
+ tenants.each do |tenant|
102
+ switch(tenant) { yield tenant }
103
+ end
104
+ end
105
+
106
+ # Establish a new connection for each specific excluded model
107
+ #
108
+ def process_excluded_models
109
+ # All other models will shared a connection (at Apartment.connection_class)
110
+ # and we can modify at will
111
+ Apartment.excluded_models.each do |excluded_model|
112
+ process_excluded_model(excluded_model)
113
+ end
114
+ end
115
+
116
+ # Reset the tenant connection to the default
117
+ #
118
+ def reset
119
+ Apartment.establish_connection @config
120
+ end
121
+
122
+ # Load the rails seed file into the db
123
+ #
124
+ def seed_data
125
+ # Don't log the output of seeding the db
126
+ silence_warnings { load_or_raise(Apartment.seed_data_file) } if Apartment.seed_data_file
127
+ end
128
+ alias seed seed_data
129
+
130
+ # Prepend the environment if configured and the environment isn't already there
131
+ #
132
+ # @param {String} tenant Database name
133
+ # @return {String} tenant name with Rails environment *optionally* prepended
134
+ #
135
+ def environmentify(tenant)
136
+ return tenant if tenant.nil? || tenant.include?(Rails.env)
137
+
138
+ if Apartment.prepend_environment
139
+ "#{Rails.env}_#{tenant}"
140
+ elsif Apartment.append_environment
141
+ "#{tenant}_#{Rails.env}"
142
+ else
143
+ tenant
144
+ end
145
+ end
146
+
147
+ protected
148
+
149
+ def process_excluded_model(excluded_model)
150
+ excluded_model.constantize.establish_connection @config
151
+ end
152
+
153
+ def drop_command(conn, tenant)
154
+ # connection.drop_database note that drop_database will not throw an exception, so manually execute
155
+ conn.execute("DROP DATABASE #{conn.quote_table_name(environmentify(tenant))}")
156
+ end
157
+
158
+ # Create the tenant
159
+ #
160
+ # @param {String} tenant Database name
161
+ #
162
+ def create_tenant(tenant)
163
+ with_neutral_connection(tenant) do |conn|
164
+ create_tenant_command(conn, tenant)
165
+ end
166
+ rescue *rescuable_exceptions => e
167
+ raise_create_tenant_error!(tenant, e)
168
+ end
169
+
170
+ def create_tenant_command(conn, tenant)
171
+ conn.create_database(environmentify(tenant), @config)
172
+ end
173
+
174
+ # Connect to new tenant
175
+ #
176
+ # @param {String} tenant Database name
177
+ #
178
+ def connect_to_new(tenant)
179
+ return reset if tenant.nil?
180
+
181
+ query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
182
+
183
+ Apartment.establish_connection multi_tenantify(tenant)
184
+ Apartment.connection.active? # call active? to manually check if this connection is valid
185
+
186
+ Apartment.connection.enable_query_cache! if query_cache_enabled
187
+ rescue *rescuable_exceptions => e
188
+ Apartment::Tenant.reset if reset_on_connection_exception?
189
+ raise_connect_error!(tenant, e)
190
+ end
191
+
192
+ # Import the database schema
193
+ #
194
+ def import_database_schema
195
+ ActiveRecord::Schema.verbose = false # do not log schema load output.
196
+
197
+ load_or_raise(Apartment.database_schema_file) if Apartment.database_schema_file
198
+ end
199
+
200
+ # Return a new config that is multi-tenanted
201
+ # @param {String} tenant: Database name
202
+ # @param {Boolean} with_database: if true, use the actual tenant's db name
203
+ # if false, use the default db name from the db
204
+ # rubocop:disable Style/OptionalBooleanParameter
205
+ def multi_tenantify(tenant, with_database = true)
206
+ db_connection_config(tenant).tap do |config|
207
+ multi_tenantify_with_tenant_db_name(config, tenant) if with_database
208
+ end
209
+ end
210
+ # rubocop:enable Style/OptionalBooleanParameter
211
+
212
+ def multi_tenantify_with_tenant_db_name(config, tenant)
213
+ config[:database] = environmentify(tenant)
214
+ end
215
+
216
+ # Load a file or raise error if it doesn't exists
217
+ #
218
+ def load_or_raise(file)
219
+ raise FileNotFound, "#{file} doesn't exist yet" unless File.exist?(file)
220
+
221
+ load(file)
222
+ end
223
+ # Backward compatibility
224
+ alias load_or_abort load_or_raise
225
+
226
+ # Exceptions to rescue from on db operations
227
+ #
228
+ def rescuable_exceptions
229
+ [ActiveRecord::ActiveRecordError] + Array(rescue_from)
230
+ end
231
+
232
+ # Extra exceptions to rescue from
233
+ #
234
+ def rescue_from
235
+ []
236
+ end
237
+
238
+ def db_connection_config(tenant)
239
+ Apartment.db_config_for(tenant).dup
240
+ end
241
+
242
+ def with_neutral_connection(tenant, &_block)
243
+ if Apartment.with_multi_server_setup
244
+ # neutral connection is necessary whenever you need to create/remove a database from a server.
245
+ # example: when you use postgresql, you need to connect to the default postgresql database before you create
246
+ # your own.
247
+ SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
248
+ yield(SeparateDbConnectionHandler.connection)
249
+ SeparateDbConnectionHandler.connection.close
250
+ else
251
+ yield(Apartment.connection)
252
+ end
253
+ end
254
+
255
+ def reset_on_connection_exception?
256
+ false
257
+ end
258
+
259
+ def raise_drop_tenant_error!(tenant, exception)
260
+ raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{exception.message}"
261
+ end
262
+
263
+ def raise_create_tenant_error!(tenant, exception)
264
+ raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{exception.message}"
265
+ end
266
+
267
+ def raise_connect_error!(tenant, exception)
268
+ raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{exception.message}"
269
+ end
270
+
271
+ class SeparateDbConnectionHandler < ::ActiveRecord::Base
272
+ end
273
+ end
274
+ end
275
+ end