ros-apartment 2.3.0 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +78 -0
  3. data/.github/workflows/changelog.yml +63 -0
  4. data/.github/workflows/reviewdog.yml +22 -0
  5. data/.pryrc +5 -3
  6. data/.rubocop.yml +32 -0
  7. data/.rubocop_todo.yml +233 -0
  8. data/.ruby-version +1 -0
  9. data/.story_branch.yml +5 -0
  10. data/Appraisals +29 -42
  11. data/CHANGELOG.md +963 -0
  12. data/Gemfile +2 -7
  13. data/Guardfile +3 -16
  14. data/HISTORY.md +159 -68
  15. data/README.md +101 -21
  16. data/Rakefile +39 -22
  17. data/TODO.md +0 -1
  18. data/gemfiles/rails_5_2.gemfile +0 -5
  19. data/gemfiles/rails_6_0.gemfile +4 -9
  20. data/gemfiles/rails_6_1.gemfile +17 -0
  21. data/gemfiles/rails_7_0.gemfile +17 -0
  22. data/gemfiles/rails_master.gemfile +3 -8
  23. data/lib/apartment/active_record/connection_handling.rb +20 -0
  24. data/lib/apartment/active_record/internal_metadata.rb +9 -0
  25. data/lib/apartment/active_record/postgresql_adapter.rb +39 -0
  26. data/lib/apartment/active_record/schema_migration.rb +11 -0
  27. data/lib/apartment/adapters/abstract_adapter.rb +52 -46
  28. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +5 -3
  29. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +3 -3
  30. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +19 -13
  31. data/lib/apartment/adapters/mysql2_adapter.rb +15 -9
  32. data/lib/apartment/adapters/postgis_adapter.rb +3 -2
  33. data/lib/apartment/adapters/postgresql_adapter.rb +79 -31
  34. data/lib/apartment/adapters/sqlite3_adapter.rb +18 -8
  35. data/lib/apartment/console.rb +23 -11
  36. data/lib/apartment/custom_console.rb +42 -0
  37. data/lib/apartment/deprecation.rb +2 -1
  38. data/lib/apartment/elevators/domain.rb +4 -3
  39. data/lib/apartment/elevators/first_subdomain.rb +3 -2
  40. data/lib/apartment/elevators/generic.rb +4 -3
  41. data/lib/apartment/elevators/host.rb +6 -1
  42. data/lib/apartment/elevators/host_hash.rb +6 -2
  43. data/lib/apartment/elevators/subdomain.rb +9 -5
  44. data/lib/apartment/log_subscriber.rb +33 -0
  45. data/lib/apartment/migrator.rb +4 -3
  46. data/lib/apartment/model.rb +29 -0
  47. data/lib/apartment/railtie.rb +16 -20
  48. data/lib/apartment/tasks/enhancements.rb +4 -6
  49. data/lib/apartment/tasks/task_helper.rb +52 -0
  50. data/lib/apartment/tenant.rb +7 -10
  51. data/lib/apartment/version.rb +3 -1
  52. data/lib/apartment.rb +56 -15
  53. data/lib/generators/apartment/install/install_generator.rb +4 -3
  54. data/lib/generators/apartment/install/templates/apartment.rb +10 -3
  55. data/lib/tasks/apartment.rake +48 -87
  56. data/ros-apartment.gemspec +59 -0
  57. metadata +148 -240
  58. data/.travis.yml +0 -65
  59. data/apartment.gemspec +0 -47
  60. data/gemfiles/rails_4_2.gemfile +0 -23
  61. data/gemfiles/rails_5_0.gemfile +0 -22
  62. data/gemfiles/rails_5_1.gemfile +0 -22
  63. data/lib/apartment/reloader.rb +0 -21
  64. data/spec/adapters/jdbc_mysql_adapter_spec.rb +0 -19
  65. data/spec/adapters/jdbc_postgresql_adapter_spec.rb +0 -41
  66. data/spec/adapters/mysql2_adapter_spec.rb +0 -59
  67. data/spec/adapters/postgresql_adapter_spec.rb +0 -61
  68. data/spec/adapters/sqlite3_adapter_spec.rb +0 -83
  69. data/spec/apartment_spec.rb +0 -11
  70. data/spec/config/database.yml.sample +0 -49
  71. data/spec/dummy/Rakefile +0 -7
  72. data/spec/dummy/app/controllers/application_controller.rb +0 -6
  73. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  74. data/spec/dummy/app/models/company.rb +0 -3
  75. data/spec/dummy/app/models/user.rb +0 -3
  76. data/spec/dummy/app/views/application/index.html.erb +0 -1
  77. data/spec/dummy/app/views/layouts/application.html.erb +0 -14
  78. data/spec/dummy/config/application.rb +0 -49
  79. data/spec/dummy/config/boot.rb +0 -11
  80. data/spec/dummy/config/database.yml.sample +0 -44
  81. data/spec/dummy/config/environment.rb +0 -5
  82. data/spec/dummy/config/environments/development.rb +0 -28
  83. data/spec/dummy/config/environments/production.rb +0 -51
  84. data/spec/dummy/config/environments/test.rb +0 -34
  85. data/spec/dummy/config/initializers/apartment.rb +0 -4
  86. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  87. data/spec/dummy/config/initializers/inflections.rb +0 -10
  88. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  89. data/spec/dummy/config/initializers/secret_token.rb +0 -7
  90. data/spec/dummy/config/initializers/session_store.rb +0 -8
  91. data/spec/dummy/config/locales/en.yml +0 -5
  92. data/spec/dummy/config/routes.rb +0 -3
  93. data/spec/dummy/config.ru +0 -4
  94. data/spec/dummy/db/migrate/20110613152810_create_dummy_models.rb +0 -39
  95. data/spec/dummy/db/migrate/20111202022214_create_table_books.rb +0 -14
  96. data/spec/dummy/db/migrate/20180415260934_create_public_tokens.rb +0 -13
  97. data/spec/dummy/db/schema.rb +0 -55
  98. data/spec/dummy/db/seeds/import.rb +0 -5
  99. data/spec/dummy/db/seeds.rb +0 -5
  100. data/spec/dummy/db/test.sqlite3 +0 -0
  101. data/spec/dummy/public/404.html +0 -26
  102. data/spec/dummy/public/422.html +0 -26
  103. data/spec/dummy/public/500.html +0 -26
  104. data/spec/dummy/public/favicon.ico +0 -0
  105. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  106. data/spec/dummy/script/rails +0 -6
  107. data/spec/dummy_engine/.gitignore +0 -8
  108. data/spec/dummy_engine/Gemfile +0 -15
  109. data/spec/dummy_engine/Rakefile +0 -34
  110. data/spec/dummy_engine/bin/rails +0 -12
  111. data/spec/dummy_engine/config/initializers/apartment.rb +0 -51
  112. data/spec/dummy_engine/dummy_engine.gemspec +0 -24
  113. data/spec/dummy_engine/lib/dummy_engine/engine.rb +0 -4
  114. data/spec/dummy_engine/lib/dummy_engine/version.rb +0 -3
  115. data/spec/dummy_engine/lib/dummy_engine.rb +0 -4
  116. data/spec/dummy_engine/test/dummy/Rakefile +0 -6
  117. data/spec/dummy_engine/test/dummy/config/application.rb +0 -22
  118. data/spec/dummy_engine/test/dummy/config/boot.rb +0 -5
  119. data/spec/dummy_engine/test/dummy/config/database.yml +0 -25
  120. data/spec/dummy_engine/test/dummy/config/environment.rb +0 -5
  121. data/spec/dummy_engine/test/dummy/config/environments/development.rb +0 -37
  122. data/spec/dummy_engine/test/dummy/config/environments/production.rb +0 -78
  123. data/spec/dummy_engine/test/dummy/config/environments/test.rb +0 -39
  124. data/spec/dummy_engine/test/dummy/config/initializers/assets.rb +0 -8
  125. data/spec/dummy_engine/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  126. data/spec/dummy_engine/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  127. data/spec/dummy_engine/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  128. data/spec/dummy_engine/test/dummy/config/initializers/inflections.rb +0 -16
  129. data/spec/dummy_engine/test/dummy/config/initializers/mime_types.rb +0 -4
  130. data/spec/dummy_engine/test/dummy/config/initializers/session_store.rb +0 -3
  131. data/spec/dummy_engine/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  132. data/spec/dummy_engine/test/dummy/config/locales/en.yml +0 -23
  133. data/spec/dummy_engine/test/dummy/config/routes.rb +0 -56
  134. data/spec/dummy_engine/test/dummy/config/secrets.yml +0 -22
  135. data/spec/dummy_engine/test/dummy/config.ru +0 -4
  136. data/spec/examples/connection_adapter_examples.rb +0 -42
  137. data/spec/examples/generic_adapter_custom_configuration_example.rb +0 -95
  138. data/spec/examples/generic_adapter_examples.rb +0 -163
  139. data/spec/examples/schema_adapter_examples.rb +0 -234
  140. data/spec/integration/apartment_rake_integration_spec.rb +0 -107
  141. data/spec/integration/query_caching_spec.rb +0 -81
  142. data/spec/integration/use_within_an_engine_spec.rb +0 -28
  143. data/spec/schemas/v1.rb +0 -16
  144. data/spec/schemas/v2.rb +0 -43
  145. data/spec/schemas/v3.rb +0 -49
  146. data/spec/spec_helper.rb +0 -61
  147. data/spec/support/apartment_helpers.rb +0 -43
  148. data/spec/support/capybara_sessions.rb +0 -15
  149. data/spec/support/config.rb +0 -10
  150. data/spec/support/contexts.rb +0 -52
  151. data/spec/support/requirements.rb +0 -35
  152. data/spec/support/setup.rb +0 -46
  153. data/spec/tasks/apartment_rake_spec.rb +0 -129
  154. data/spec/tenant_spec.rb +0 -190
  155. data/spec/unit/config_spec.rb +0 -112
  156. data/spec/unit/elevators/domain_spec.rb +0 -32
  157. data/spec/unit/elevators/first_subdomain_spec.rb +0 -24
  158. data/spec/unit/elevators/generic_spec.rb +0 -54
  159. data/spec/unit/elevators/host_hash_spec.rb +0 -32
  160. data/spec/unit/elevators/host_spec.rb +0 -89
  161. data/spec/unit/elevators/subdomain_spec.rb +0 -76
  162. data/spec/unit/migrator_spec.rb +0 -77
  163. data/spec/unit/reloader_spec.rb +0 -24
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Apartment
2
4
  module Adapters
5
+ # Abstract adapter from which all the Apartment DB related adapters will inherit the base logic
3
6
  class AbstractAdapter
4
7
  include ActiveSupport::Callbacks
5
8
  define_callbacks :create, :switch
@@ -32,6 +35,12 @@ module Apartment
32
35
  end
33
36
  end
34
37
 
38
+ # Initialize Apartment config options such as excluded_models
39
+ #
40
+ def init
41
+ process_excluded_models
42
+ end
43
+
35
44
  # Note alias_method here doesn't work with inheritence apparently ??
36
45
  #
37
46
  def current
@@ -45,7 +54,6 @@ module Apartment
45
54
  def default_tenant
46
55
  @default_tenant || Apartment.default_tenant
47
56
  end
48
- alias :default_schema :default_tenant # TODO deprecate default_schema
49
57
 
50
58
  # Drop the tenant
51
59
  #
@@ -55,9 +63,8 @@ module Apartment
55
63
  with_neutral_connection(tenant) do |conn|
56
64
  drop_command(conn, tenant)
57
65
  end
58
-
59
- rescue *rescuable_exceptions => exception
60
- raise_drop_tenant_error!(tenant, exception)
66
+ rescue *rescuable_exceptions => e
67
+ raise_drop_tenant_error!(tenant, e)
61
68
  end
62
69
 
63
70
  # Switch to a new tenant
@@ -66,8 +73,6 @@ module Apartment
66
73
  #
67
74
  def switch!(tenant = nil)
68
75
  run_callbacks :switch do
69
- return reset if tenant.nil?
70
-
71
76
  connect_to_new(tenant).tap do
72
77
  Apartment.connection.clear_query_cache
73
78
  end
@@ -79,13 +84,14 @@ module Apartment
79
84
  # @param {String?} tenant to connect to
80
85
  #
81
86
  def switch(tenant = nil)
87
+ previous_tenant = current
88
+ switch!(tenant)
89
+ yield
90
+ ensure
82
91
  begin
83
- previous_tenant = current
84
- switch!(tenant)
85
- yield
86
-
87
- ensure
88
- switch!(previous_tenant) rescue reset
92
+ switch!(previous_tenant)
93
+ rescue StandardError => _e
94
+ reset
89
95
  end
90
96
  end
91
97
 
@@ -93,14 +99,15 @@ module Apartment
93
99
  #
94
100
  def each(tenants = Apartment.tenant_names)
95
101
  tenants.each do |tenant|
96
- switch(tenant){ yield tenant }
102
+ switch(tenant) { yield tenant }
97
103
  end
98
104
  end
99
105
 
100
106
  # Establish a new connection for each specific excluded model
101
107
  #
102
108
  def process_excluded_models
103
- # All other models will shared a connection (at Apartment.connection_class) and we can modify at will
109
+ # All other models will shared a connection (at Apartment.connection_class)
110
+ # and we can modify at will
104
111
  Apartment.excluded_models.each do |excluded_model|
105
112
  process_excluded_model(excluded_model)
106
113
  end
@@ -116,9 +123,9 @@ module Apartment
116
123
  #
117
124
  def seed_data
118
125
  # Don't log the output of seeding the db
119
- silence_warnings{ load_or_raise(Apartment.seed_data_file) } if Apartment.seed_data_file
126
+ silence_warnings { load_or_raise(Apartment.seed_data_file) } if Apartment.seed_data_file
120
127
  end
121
- alias_method :seed, :seed_data
128
+ alias seed seed_data
122
129
 
123
130
  # Prepend the environment if configured and the environment isn't already there
124
131
  #
@@ -126,20 +133,18 @@ module Apartment
126
133
  # @return {String} tenant name with Rails environment *optionally* prepended
127
134
  #
128
135
  def environmentify(tenant)
129
- unless tenant.include?(Rails.env)
130
- if Apartment.prepend_environment
131
- "#{Rails.env}_#{tenant}"
132
- elsif Apartment.append_environment
133
- "#{tenant}_#{Rails.env}"
134
- else
135
- tenant
136
- end
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}"
137
142
  else
138
143
  tenant
139
144
  end
140
145
  end
141
146
 
142
- protected
147
+ protected
143
148
 
144
149
  def process_excluded_model(excluded_model)
145
150
  excluded_model.constantize.establish_connection @config
@@ -158,8 +163,8 @@ module Apartment
158
163
  with_neutral_connection(tenant) do |conn|
159
164
  create_tenant_command(conn, tenant)
160
165
  end
161
- rescue *rescuable_exceptions => exception
162
- raise_create_tenant_error!(tenant, exception)
166
+ rescue *rescuable_exceptions => e
167
+ raise_create_tenant_error!(tenant, e)
163
168
  end
164
169
 
165
170
  def create_tenant_command(conn, tenant)
@@ -171,21 +176,23 @@ module Apartment
171
176
  # @param {String} tenant Database name
172
177
  #
173
178
  def connect_to_new(tenant)
179
+ return reset if tenant.nil?
180
+
174
181
  query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
175
182
 
176
183
  Apartment.establish_connection multi_tenantify(tenant)
177
- Apartment.connection.active? # call active? to manually check if this connection is valid
184
+ Apartment.connection.active? # call active? to manually check if this connection is valid
178
185
 
179
186
  Apartment.connection.enable_query_cache! if query_cache_enabled
180
- rescue *rescuable_exceptions => exception
187
+ rescue *rescuable_exceptions => e
181
188
  Apartment::Tenant.reset if reset_on_connection_exception?
182
- raise_connect_error!(tenant, exception)
189
+ raise_connect_error!(tenant, e)
183
190
  end
184
191
 
185
192
  # Import the database schema
186
193
  #
187
194
  def import_database_schema
188
- ActiveRecord::Schema.verbose = false # do not log schema load output.
195
+ ActiveRecord::Schema.verbose = false # do not log schema load output.
189
196
 
190
197
  load_or_raise(Apartment.database_schema_file) if Apartment.database_schema_file
191
198
  end
@@ -194,13 +201,13 @@ module Apartment
194
201
  # @param {String} tenant: Database name
195
202
  # @param {Boolean} with_database: if true, use the actual tenant's db name
196
203
  # if false, use the default db name from the db
204
+ # rubocop:disable Style/OptionalBooleanParameter
197
205
  def multi_tenantify(tenant, with_database = true)
198
206
  db_connection_config(tenant).tap do |config|
199
- if with_database
200
- multi_tenantify_with_tenant_db_name(config, tenant)
201
- end
207
+ multi_tenantify_with_tenant_db_name(config, tenant) if with_database
202
208
  end
203
209
  end
210
+ # rubocop:enable Style/OptionalBooleanParameter
204
211
 
205
212
  def multi_tenantify_with_tenant_db_name(config, tenant)
206
213
  config[:database] = environmentify(tenant)
@@ -209,14 +216,12 @@ module Apartment
209
216
  # Load a file or raise error if it doesn't exists
210
217
  #
211
218
  def load_or_raise(file)
212
- if File.exists?(file)
213
- load(file)
214
- else
215
- raise FileNotFound, "#{file} doesn't exist yet"
216
- end
219
+ raise FileNotFound, "#{file} doesn't exist yet" unless File.exist?(file)
220
+
221
+ load(file)
217
222
  end
218
223
  # Backward compatibility
219
- alias_method :load_or_abort, :load_or_raise
224
+ alias load_or_abort load_or_raise
220
225
 
221
226
  # Exceptions to rescue from on db operations
222
227
  #
@@ -231,13 +236,14 @@ module Apartment
231
236
  end
232
237
 
233
238
  def db_connection_config(tenant)
234
- Apartment.db_config_for(tenant).clone
239
+ Apartment.db_config_for(tenant).dup
235
240
  end
236
241
 
237
- def with_neutral_connection(tenant, &block)
242
+ def with_neutral_connection(tenant, &_block)
238
243
  if Apartment.with_multi_server_setup
239
244
  # neutral connection is necessary whenever you need to create/remove a database from a server.
240
- # example: when you use postgresql, you need to connect to the default postgresql database before you create your own.
245
+ # example: when you use postgresql, you need to connect to the default postgresql database before you create
246
+ # your own.
241
247
  SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
242
248
  yield(SeparateDbConnectionHandler.connection)
243
249
  SeparateDbConnectionHandler.connection.close
@@ -251,15 +257,15 @@ module Apartment
251
257
  end
252
258
 
253
259
  def raise_drop_tenant_error!(tenant, exception)
254
- raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{ exception.message }"
260
+ raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{exception.message}"
255
261
  end
256
262
 
257
263
  def raise_create_tenant_error!(tenant, exception)
258
- raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{ exception.message }"
264
+ raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{exception.message}"
259
265
  end
260
266
 
261
267
  def raise_connect_error!(tenant, exception)
262
- raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{ exception.message }"
268
+ raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{exception.message}"
263
269
  end
264
270
 
265
271
  class SeparateDbConnectionHandler < ::ActiveRecord::Base
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'apartment/adapters/abstract_adapter'
2
4
 
3
5
  module Apartment
4
6
  module Adapters
7
+ # JDBC Abstract adapter
5
8
  class AbstractJDBCAdapter < AbstractAdapter
6
-
7
- private
9
+ private
8
10
 
9
11
  def multi_tenantify_with_tenant_db_name(config, tenant)
10
- config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
12
+ config[:url] = "#{config[:url].gsub(%r{(\S+)/.+$}, '\1')}/#{environmentify(tenant)}"
11
13
  end
12
14
 
13
15
  def rescue_from
@@ -1,7 +1,8 @@
1
- require "apartment/adapters/abstract_jdbc_adapter"
1
+ # frozen_string_literal: true
2
2
 
3
- module Apartment
3
+ require 'apartment/adapters/abstract_jdbc_adapter'
4
4
 
5
+ module Apartment
5
6
  module Tenant
6
7
  def self.jdbc_mysql_adapter(config)
7
8
  Adapters::JDBCMysqlAdapter.new config
@@ -10,7 +11,6 @@ module Apartment
10
11
 
11
12
  module Adapters
12
13
  class JDBCMysqlAdapter < AbstractJDBCAdapter
13
-
14
14
  def reset_on_connection_exception?
15
15
  true
16
16
  end
@@ -1,28 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'apartment/adapters/postgresql_adapter'
2
4
 
3
5
  module Apartment
6
+ # JDBC helper to decide wether to use JDBC Postgresql Adapter or JDBC Postgresql Adapter with Schemas
4
7
  module Tenant
5
-
6
8
  def self.jdbc_postgresql_adapter(config)
7
- Apartment.use_schemas ?
8
- Adapters::JDBCPostgresqlSchemaAdapter.new(config) :
9
+ if Apartment.use_schemas
10
+ Adapters::JDBCPostgresqlSchemaAdapter.new(config)
11
+ else
9
12
  Adapters::JDBCPostgresqlAdapter.new(config)
13
+ end
10
14
  end
11
15
  end
12
16
 
13
17
  module Adapters
14
-
15
18
  # Default adapter when not using Postgresql Schemas
16
19
  class JDBCPostgresqlAdapter < PostgresqlAdapter
17
-
18
- private
20
+ private
19
21
 
20
22
  def multi_tenantify_with_tenant_db_name(config, tenant)
21
- config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
23
+ config[:url] = "#{config[:url].gsub(%r{(\S+)/.+$}, '\1')}/#{environmentify(tenant)}"
22
24
  end
23
25
 
24
26
  def create_tenant_command(conn, tenant)
25
- conn.create_database(environmentify(tenant), { :thisisahack => '' })
27
+ conn.create_database(environmentify(tenant), thisisahack: '')
26
28
  end
27
29
 
28
30
  def rescue_from
@@ -32,21 +34,25 @@ module Apartment
32
34
 
33
35
  # Separate Adapter for Postgresql when using schemas
34
36
  class JDBCPostgresqlSchemaAdapter < PostgresqlSchemaAdapter
35
-
36
37
  # Set schema search path to new schema
37
38
  #
38
39
  def connect_to_new(tenant = nil)
39
40
  return reset if tenant.nil?
40
- raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.all_schemas.include? tenant.to_s
41
+ raise ActiveRecord::StatementInvalid, "Could not find schema #{tenant}" unless schema_exists?(tenant)
41
42
 
42
- @current = tenant.to_s
43
+ @current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
43
44
  Apartment.connection.schema_search_path = full_search_path
44
-
45
45
  rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError
46
46
  raise TenantNotFound, "One of the following schema(s) is invalid: #{full_search_path}"
47
47
  end
48
48
 
49
- private
49
+ private
50
+
51
+ def tenant_exists?(tenant)
52
+ return true unless Apartment.tenant_presence_check
53
+
54
+ Apartment.connection.all_schemas.include? tenant
55
+ end
50
56
 
51
57
  def rescue_from
52
58
  ActiveRecord::JDBCError
@@ -1,31 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'apartment/adapters/abstract_adapter'
2
4
 
3
5
  module Apartment
6
+ # Helper module to decide wether to use mysql2 adapter or mysql2 adapter with schemas
4
7
  module Tenant
5
-
6
8
  def self.mysql2_adapter(config)
7
- Apartment.use_schemas ?
8
- Adapters::Mysql2SchemaAdapter.new(config) :
9
+ if Apartment.use_schemas
10
+ Adapters::Mysql2SchemaAdapter.new(config)
11
+ else
9
12
  Adapters::Mysql2Adapter.new(config)
13
+ end
10
14
  end
11
15
  end
12
16
 
13
17
  module Adapters
18
+ # Mysql2 Adapter
14
19
  class Mysql2Adapter < AbstractAdapter
15
-
16
20
  def initialize(config)
17
21
  super
18
22
 
19
23
  @default_tenant = config[:database]
20
24
  end
21
25
 
22
- protected
26
+ protected
23
27
 
24
28
  def rescue_from
25
29
  Mysql2::Error
26
30
  end
27
31
  end
28
32
 
33
+ # Mysql2 Schemas Adapter
29
34
  class Mysql2SchemaAdapter < AbstractAdapter
30
35
  def initialize(config)
31
36
  super
@@ -37,10 +42,12 @@ module Apartment
37
42
  # Reset current tenant to the default_tenant
38
43
  #
39
44
  def reset
45
+ return unless default_tenant
46
+
40
47
  Apartment.connection.execute "use `#{default_tenant}`"
41
48
  end
42
49
 
43
- protected
50
+ protected
44
51
 
45
52
  # Connect to new tenant
46
53
  #
@@ -48,10 +55,9 @@ module Apartment
48
55
  return reset if tenant.nil?
49
56
 
50
57
  Apartment.connection.execute "use `#{environmentify(tenant)}`"
51
-
52
- rescue ActiveRecord::StatementInvalid => exception
58
+ rescue ActiveRecord::StatementInvalid => e
53
59
  Apartment::Tenant.reset
54
- raise_connect_error!(tenant, exception)
60
+ raise_connect_error!(tenant, e)
55
61
  end
56
62
 
57
63
  def process_excluded_model(model)
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # handle postgis adapter as if it were postgresql,
2
4
  # only override the adapter_method used for initialization
3
- require "apartment/adapters/postgresql_adapter"
5
+ require 'apartment/adapters/postgresql_adapter'
4
6
 
5
7
  module Apartment
6
8
  module Tenant
7
-
8
9
  def self.postgis_adapter(config)
9
10
  postgresql_adapter(config)
10
11
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'apartment/adapters/abstract_adapter'
4
+ require 'apartment/active_record/postgresql_adapter'
2
5
 
3
6
  module Apartment
4
7
  module Tenant
5
-
6
8
  def self.postgresql_adapter(config)
7
9
  adapter = Adapters::PostgresqlAdapter
8
10
  adapter = Adapters::PostgresqlSchemaAdapter if Apartment.use_schemas
@@ -14,8 +16,7 @@ module Apartment
14
16
  module Adapters
15
17
  # Default adapter when not using Postgresql Schemas
16
18
  class PostgresqlAdapter < AbstractAdapter
17
-
18
- private
19
+ private
19
20
 
20
21
  def rescue_from
21
22
  PG::Error
@@ -24,13 +25,16 @@ module Apartment
24
25
 
25
26
  # Separate Adapter for Postgresql when using schemas
26
27
  class PostgresqlSchemaAdapter < AbstractAdapter
27
-
28
28
  def initialize(config)
29
29
  super
30
30
 
31
31
  reset
32
32
  end
33
33
 
34
+ def default_tenant
35
+ @default_tenant = Apartment.default_tenant || 'public'
36
+ end
37
+
34
38
  # Reset schema search path to the default schema_search_path
35
39
  #
36
40
  # @return {String} default schema search path
@@ -40,11 +44,16 @@ module Apartment
40
44
  Apartment.connection.schema_search_path = full_search_path
41
45
  end
42
46
 
47
+ def init
48
+ super
49
+ Apartment.connection.schema_search_path = full_search_path
50
+ end
51
+
43
52
  def current
44
53
  @current || default_tenant
45
54
  end
46
55
 
47
- protected
56
+ protected
48
57
 
49
58
  def process_excluded_model(excluded_model)
50
59
  excluded_model.constantize.tap do |klass|
@@ -56,39 +65,58 @@ module Apartment
56
65
  end
57
66
 
58
67
  def drop_command(conn, tenant)
59
- conn.execute(%{DROP SCHEMA "#{tenant}" CASCADE})
68
+ conn.execute(%(DROP SCHEMA "#{tenant}" CASCADE))
60
69
  end
61
70
 
62
71
  # Set schema search path to new schema
63
72
  #
64
73
  def connect_to_new(tenant = nil)
65
74
  return reset if tenant.nil?
66
- raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.schema_exists?(tenant.to_s)
75
+ raise ActiveRecord::StatementInvalid, "Could not find schema #{tenant}" unless schema_exists?(tenant)
67
76
 
68
- @current = tenant.to_s
77
+ @current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
69
78
  Apartment.connection.schema_search_path = full_search_path
70
79
 
71
80
  # When the PostgreSQL version is < 9.3,
72
81
  # there is a issue for prepared statement with changing search_path.
73
82
  # https://www.postgresql.org/docs/9.3/static/sql-prepare.html
74
- if postgresql_version < 90300
75
- Apartment.connection.clear_cache!
76
- end
77
-
78
- rescue *rescuable_exceptions
79
- raise TenantNotFound, "One of the following schema(s) is invalid: \"#{tenant}\" #{full_search_path}"
83
+ Apartment.connection.clear_cache! if postgresql_version < 90_300
84
+ rescue *rescuable_exceptions => e
85
+ raise_schema_connect_to_new(tenant, e)
80
86
  end
81
87
 
82
- private
88
+ private
89
+
90
+ def tenant_exists?(tenant)
91
+ return true unless Apartment.tenant_presence_check
92
+
93
+ Apartment.connection.schema_exists?(tenant)
94
+ end
83
95
 
84
96
  def create_tenant_command(conn, tenant)
85
- conn.execute(%{CREATE SCHEMA "#{tenant}"})
97
+ # NOTE: This was causing some tests to fail because of the database strategy for rspec
98
+ if ActiveRecord::Base.connection.open_transactions.positive?
99
+ conn.execute(%(CREATE SCHEMA "#{tenant}"))
100
+ else
101
+ schema = %(BEGIN;
102
+ CREATE SCHEMA "#{tenant}";
103
+ COMMIT;)
104
+
105
+ conn.execute(schema)
106
+ end
107
+ rescue *rescuable_exceptions => e
108
+ rollback_transaction(conn)
109
+ raise e
110
+ end
111
+
112
+ def rollback_transaction(conn)
113
+ conn.execute('ROLLBACK;')
86
114
  end
87
115
 
88
116
  # Generate the final search path to set including persistent_schemas
89
117
  #
90
118
  def full_search_path
91
- persistent_schemas.map(&:inspect).join(", ")
119
+ persistent_schemas.map(&:inspect).join(', ')
92
120
  end
93
121
 
94
122
  def persistent_schemas
@@ -100,20 +128,33 @@ module Apartment
100
128
  # public from Rails 5.0.
101
129
  Apartment.connection.send(:postgresql_version)
102
130
  end
131
+
132
+ def schema_exists?(schemas)
133
+ return true unless Apartment.tenant_presence_check
134
+
135
+ Array(schemas).all? { |schema| Apartment.connection.schema_exists?(schema.to_s) }
136
+ end
137
+
138
+ def raise_schema_connect_to_new(tenant, exception)
139
+ raise TenantNotFound, <<~EXCEPTION_MESSAGE
140
+ Could not set search path to schemas, they may be invalid: "#{tenant}" #{full_search_path}.
141
+ Original error: #{exception.class}: #{exception}
142
+ EXCEPTION_MESSAGE
143
+ end
103
144
  end
104
145
 
105
146
  # Another Adapter for Postgresql when using schemas and SQL
106
147
  class PostgresqlSchemaFromSqlAdapter < PostgresqlSchemaAdapter
107
-
108
- PSQL_DUMP_BLACKLISTED_STATEMENTS= [
148
+ PSQL_DUMP_BLACKLISTED_STATEMENTS = [
109
149
  /SET search_path/i, # overridden later
110
150
  /SET lock_timeout/i, # new in postgresql 9.3
111
151
  /SET row_security/i, # new in postgresql 9.5
112
152
  /SET idle_in_transaction_session_timeout/i, # new in postgresql 9.6
153
+ /SET default_table_access_method/i, # new in postgresql 12
113
154
  /CREATE SCHEMA public/i,
114
- /COMMENT ON SCHEMA public/i,
155
+ /COMMENT ON SCHEMA public/i
115
156
 
116
- ]
157
+ ].freeze
117
158
 
118
159
  def import_database_schema
119
160
  preserving_search_path do
@@ -122,14 +163,14 @@ module Apartment
122
163
  end
123
164
  end
124
165
 
125
- private
166
+ private
126
167
 
127
168
  # Re-set search path after the schema is imported.
128
169
  # Postgres now sets search path to empty before dumping the schema
129
170
  # and it mut be reset
130
171
  #
131
172
  def preserving_search_path
132
- search_path = Apartment.connection.execute("show search_path").first["search_path"]
173
+ search_path = Apartment.connection.execute('show search_path').first['search_path']
133
174
  yield
134
175
  Apartment.connection.execute("set search_path = #{search_path}")
135
176
  end
@@ -153,7 +194,6 @@ module Apartment
153
194
  # @return {String} raw SQL contaning only postgres schema dump
154
195
  #
155
196
  def pg_dump_schema
156
-
157
197
  # Skip excluded tables? :/
158
198
  # excluded_tables =
159
199
  # collect_table_names(Apartment.excluded_models)
@@ -169,23 +209,31 @@ module Apartment
169
209
  #
170
210
  # @return {String} raw SQL contaning inserts with data from schema_migrations
171
211
  #
212
+ # rubocop:disable Layout/LineLength
172
213
  def pg_dump_schema_migrations_data
173
214
  with_pg_env { `pg_dump -a --inserts -t #{default_tenant}.schema_migrations -t #{default_tenant}.ar_internal_metadata #{dbname}` }
174
215
  end
216
+ # rubocop:enable Layout/LineLength
175
217
 
176
218
  # Temporary set Postgresql related environment variables if there are in @config
177
219
  #
178
- def with_pg_env(&block)
179
- pghost, pgport, pguser, pgpassword = ENV['PGHOST'], ENV['PGPORT'], ENV['PGUSER'], ENV['PGPASSWORD']
220
+ def with_pg_env
221
+ pghost = ENV['PGHOST']
222
+ pgport = ENV['PGPORT']
223
+ pguser = ENV['PGUSER']
224
+ pgpassword = ENV['PGPASSWORD']
180
225
 
181
226
  ENV['PGHOST'] = @config[:host] if @config[:host]
182
227
  ENV['PGPORT'] = @config[:port].to_s if @config[:port]
183
228
  ENV['PGUSER'] = @config[:username].to_s if @config[:username]
184
229
  ENV['PGPASSWORD'] = @config[:password].to_s if @config[:password]
185
230
 
186
- block.call
231
+ yield
187
232
  ensure
188
- ENV['PGHOST'], ENV['PGPORT'], ENV['PGUSER'], ENV['PGPASSWORD'] = pghost, pgport, pguser, pgpassword
233
+ ENV['PGHOST'] = pghost
234
+ ENV['PGPORT'] = pgport
235
+ ENV['PGUSER'] = pguser
236
+ ENV['PGPASSWORD'] = pgpassword
189
237
  end
190
238
 
191
239
  # Remove "SET search_path ..." line from SQL dump and prepend search_path set to current tenant
@@ -197,7 +245,7 @@ module Apartment
197
245
 
198
246
  swap_schema_qualifier(sql)
199
247
  .split("\n")
200
- .select {|line| check_input_against_regexps(line, PSQL_DUMP_BLACKLISTED_STATEMENTS).empty?}
248
+ .select { |line| check_input_against_regexps(line, PSQL_DUMP_BLACKLISTED_STATEMENTS).empty? }
201
249
  .prepend(search_path)
202
250
  .join("\n")
203
251
  end
@@ -207,7 +255,7 @@ module Apartment
207
255
  if Apartment.pg_excluded_names.any? { |name| match.include? name }
208
256
  match
209
257
  else
210
- match.gsub("#{default_tenant}.", %{"#{current}".})
258
+ match.gsub("#{default_tenant}.", %("#{current}".))
211
259
  end
212
260
  end
213
261
  end
@@ -215,7 +263,7 @@ module Apartment
215
263
  # Checks if any of regexps matches against input
216
264
  #
217
265
  def check_input_against_regexps(input, regexps)
218
- regexps.select {|c| input.match c}
266
+ regexps.select { |c| input.match c }
219
267
  end
220
268
 
221
269
  # Collect table names from AR Models