ros-apartment 2.7.1 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +78 -0
  3. data/.github/workflows/changelog.yml +63 -0
  4. data/.rubocop.yml +78 -4
  5. data/.rubocop_todo.yml +50 -13
  6. data/CHANGELOG.md +963 -0
  7. data/Gemfile +2 -2
  8. data/Guardfile +0 -15
  9. data/HISTORY.md +93 -92
  10. data/README.md +27 -0
  11. data/Rakefile +5 -2
  12. data/gemfiles/rails_5_0.gemfile +1 -1
  13. data/gemfiles/rails_5_1.gemfile +1 -1
  14. data/gemfiles/rails_5_2.gemfile +1 -1
  15. data/gemfiles/rails_6_0.gemfile +1 -1
  16. data/gemfiles/rails_master.gemfile +1 -1
  17. data/lib/apartment.rb +5 -3
  18. data/lib/apartment/active_record/connection_handling.rb +3 -0
  19. data/lib/apartment/active_record/internal_metadata.rb +0 -2
  20. data/lib/apartment/active_record/schema_migration.rb +0 -2
  21. data/lib/apartment/adapters/abstract_adapter.rb +5 -3
  22. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +2 -1
  23. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +2 -1
  24. data/lib/apartment/adapters/mysql2_adapter.rb +3 -0
  25. data/lib/apartment/adapters/postgresql_adapter.rb +30 -7
  26. data/lib/apartment/console.rb +5 -9
  27. data/lib/apartment/custom_console.rb +2 -2
  28. data/lib/apartment/log_subscriber.rb +33 -0
  29. data/lib/apartment/railtie.rb +26 -21
  30. data/lib/apartment/tasks/task_helper.rb +7 -2
  31. data/lib/apartment/tenant.rb +7 -20
  32. data/lib/apartment/version.rb +1 -1
  33. data/lib/generators/apartment/install/templates/apartment.rb +2 -1
  34. data/lib/tasks/apartment.rake +4 -7
  35. data/ros-apartment.gemspec +2 -2
  36. metadata +12 -10
  37. data/.travis.yml +0 -49
  38. data/lib/apartment/active_record/log_subscriber.rb +0 -41
data/README.md CHANGED
@@ -338,6 +338,23 @@ Apartment.configure do |config|
338
338
  end
339
339
  ```
340
340
 
341
+ ### Additional logging information
342
+
343
+ Enabling this configuration will output the database that the process is currently connected to as well as which
344
+ schemas are in the search path. This can be enabled by setting to true the `active_record_log` configuration.
345
+
346
+ Please note that our custom logger inherits from `ActiveRecord::LogSubscriber` so this will be required for the configuration to work.
347
+
348
+ **Example log output:**
349
+
350
+ <img src="documentation/images/log_example.png">
351
+
352
+ ```ruby
353
+ Apartment.configure do |config|
354
+ config.active_record_log = true
355
+ end
356
+ ```
357
+
341
358
  ### Excluding models
342
359
 
343
360
  If you have some models that should always access the 'public' tenant, 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:
@@ -594,6 +611,16 @@ module Apartment
594
611
  end
595
612
  ```
596
613
 
614
+ ## Running rails console without a connection to the database
615
+
616
+ By default, once apartment starts, it establishes a connection to the database. It is possible to
617
+ disable this initial connection, by running with `APARTMENT_DISABLE_INIT` set to something:
618
+
619
+ ```shell
620
+ $ APARTMENT_DISABLE_INIT=true DATABASE_URL=postgresql://localhost:1234/buk_development bin/rails runner 'puts 1'
621
+ # 1
622
+ ```
623
+
597
624
  ## Contributing
598
625
 
599
626
  * In both `spec/dummy/config` and `spec/config`, you will see `database.yml.sample` files
data/Rakefile CHANGED
@@ -46,8 +46,10 @@ namespace :db do
46
46
  apartment_db_file = 'spec/config/database.yml'
47
47
  rails_db_file = 'spec/dummy/config/database.yml'
48
48
 
49
- FileUtils.copy(apartment_db_file + '.sample', apartment_db_file, verbose: true) unless File.exist?(apartment_db_file)
50
- FileUtils.copy(rails_db_file + '.sample', rails_db_file, verbose: true) unless File.exist?(rails_db_file)
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)
51
53
  end
52
54
  end
53
55
 
@@ -63,6 +65,7 @@ namespace :postgres do
63
65
  params << "-U#{pg_config['username']}"
64
66
  params << "-h#{pg_config['host']}" if pg_config['host']
65
67
  params << "-p#{pg_config['port']}" if pg_config['port']
68
+
66
69
  begin
67
70
  `createdb #{params.join(' ')}`
68
71
  rescue StandardError
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "perx-rubocop", "~> 0.0.3"
6
5
  gem "rails", "~> 5.0.0"
6
+ gem "rubocop"
7
7
 
8
8
  group :local do
9
9
  gem "guard-rspec", "~> 4.2"
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "perx-rubocop", "~> 0.0.3"
6
5
  gem "rails", "~> 5.1.0"
6
+ gem "rubocop"
7
7
 
8
8
  group :local do
9
9
  gem "guard-rspec", "~> 4.2"
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "perx-rubocop", "~> 0.0.3"
6
5
  gem "rails", "~> 5.2.0"
6
+ gem "rubocop"
7
7
 
8
8
  group :local do
9
9
  gem "guard-rspec", "~> 4.2"
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "perx-rubocop", "~> 0.0.3"
6
5
  gem "rails", "~> 6.0.0"
6
+ gem "rubocop"
7
7
 
8
8
  group :local do
9
9
  gem "guard-rspec", "~> 4.2"
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "perx-rubocop", "~> 0.0.3"
6
5
  gem "rails", git: "https://github.com/rails/rails.git"
6
+ gem "rubocop"
7
7
 
8
8
  group :local do
9
9
  gem "guard-rspec", "~> 4.2"
@@ -6,16 +6,18 @@ require 'forwardable'
6
6
  require 'active_record'
7
7
  require 'apartment/tenant'
8
8
 
9
- # require_relative 'apartment/arel/visitors/postgresql'
9
+ require_relative 'apartment/log_subscriber'
10
10
 
11
- require_relative 'apartment/active_record/log_subscriber'
12
- require_relative 'apartment/active_record/connection_handling' if ActiveRecord.version.release >= Gem::Version.new('6.0')
11
+ if ActiveRecord.version.release >= Gem::Version.new('6.0')
12
+ require_relative 'apartment/active_record/connection_handling'
13
+ end
13
14
 
14
15
  if ActiveRecord.version.release >= Gem::Version.new('6.1')
15
16
  require_relative 'apartment/active_record/schema_migration'
16
17
  require_relative 'apartment/active_record/internal_metadata'
17
18
  end
18
19
 
20
+ # Apartment main definitions
19
21
  module Apartment
20
22
  class << self
21
23
  extend Forwardable
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
+ # This is monkeypatching activerecord 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
4
7
  module ConnectionHandling
5
8
  def connected_to_with_tenant(database: nil, role: nil, prevent_writes: false, &blk)
6
9
  current_tenant = Apartment::Tenant.current
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Rails/ApplicationRecord
4
3
  class InternalMetadata < ActiveRecord::Base # :nodoc:
5
4
  class << self
6
5
  def table_exists?
@@ -8,4 +7,3 @@ class InternalMetadata < ActiveRecord::Base # :nodoc:
8
7
  end
9
8
  end
10
9
  end
11
- # rubocop:enable Rails/ApplicationRecord
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
- # rubocop:disable Rails/ApplicationRecord
5
4
  class SchemaMigration < ActiveRecord::Base # :nodoc:
6
5
  class << self
7
6
  def table_exists?
@@ -9,5 +8,4 @@ module ActiveRecord
9
8
  end
10
9
  end
11
10
  end
12
- # rubocop:enable Rails/ApplicationRecord
13
11
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Apartment
4
4
  module Adapters
5
- # rubocop:disable Metrics/ClassLength
5
+ # Abstract adapter from which all the Apartment DB related adapters will inherit the base logic
6
6
  class AbstractAdapter
7
7
  include ActiveSupport::Callbacks
8
8
  define_callbacks :create, :switch
@@ -201,11 +201,13 @@ module Apartment
201
201
  # @param {String} tenant: Database name
202
202
  # @param {Boolean} with_database: if true, use the actual tenant's db name
203
203
  # if false, use the default db name from the db
204
+ # rubocop:disable Style/OptionalBooleanParameter
204
205
  def multi_tenantify(tenant, with_database = true)
205
206
  db_connection_config(tenant).tap do |config|
206
207
  multi_tenantify_with_tenant_db_name(config, tenant) if with_database
207
208
  end
208
209
  end
210
+ # rubocop:enable Style/OptionalBooleanParameter
209
211
 
210
212
  def multi_tenantify_with_tenant_db_name(config, tenant)
211
213
  config[:database] = environmentify(tenant)
@@ -240,7 +242,8 @@ module Apartment
240
242
  def with_neutral_connection(tenant, &_block)
241
243
  if Apartment.with_multi_server_setup
242
244
  # neutral connection is necessary whenever you need to create/remove a database from a server.
243
- # 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.
244
247
  SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
245
248
  yield(SeparateDbConnectionHandler.connection)
246
249
  SeparateDbConnectionHandler.connection.close
@@ -269,5 +272,4 @@ module Apartment
269
272
  end
270
273
  end
271
274
  end
272
- # rubocop:enable Metrics/ClassLength
273
275
  end
@@ -4,11 +4,12 @@ require 'apartment/adapters/abstract_adapter'
4
4
 
5
5
  module Apartment
6
6
  module Adapters
7
+ # JDBC Abstract adapter
7
8
  class AbstractJDBCAdapter < AbstractAdapter
8
9
  private
9
10
 
10
11
  def multi_tenantify_with_tenant_db_name(config, tenant)
11
- config[:url] = "#{config[:url].gsub(%r{(\S+)\/.+$}, '\1')}/#{environmentify(tenant)}"
12
+ config[:url] = "#{config[:url].gsub(%r{(\S+)/.+$}, '\1')}/#{environmentify(tenant)}"
12
13
  end
13
14
 
14
15
  def rescue_from
@@ -3,6 +3,7 @@
3
3
  require 'apartment/adapters/postgresql_adapter'
4
4
 
5
5
  module Apartment
6
+ # JDBC helper to decide wether to use JDBC Postgresql Adapter or JDBC Postgresql Adapter with Schemas
6
7
  module Tenant
7
8
  def self.jdbc_postgresql_adapter(config)
8
9
  if Apartment.use_schemas
@@ -19,7 +20,7 @@ module Apartment
19
20
  private
20
21
 
21
22
  def multi_tenantify_with_tenant_db_name(config, tenant)
22
- config[:url] = "#{config[:url].gsub(%r{(\S+)\/.+$}, '\1')}/#{environmentify(tenant)}"
23
+ config[:url] = "#{config[:url].gsub(%r{(\S+)/.+$}, '\1')}/#{environmentify(tenant)}"
23
24
  end
24
25
 
25
26
  def create_tenant_command(conn, tenant)
@@ -3,6 +3,7 @@
3
3
  require 'apartment/adapters/abstract_adapter'
4
4
 
5
5
  module Apartment
6
+ # Helper module to decide wether to use mysql2 adapter or mysql2 adapter with schemas
6
7
  module Tenant
7
8
  def self.mysql2_adapter(config)
8
9
  if Apartment.use_schemas
@@ -14,6 +15,7 @@ module Apartment
14
15
  end
15
16
 
16
17
  module Adapters
18
+ # Mysql2 Adapter
17
19
  class Mysql2Adapter < AbstractAdapter
18
20
  def initialize(config)
19
21
  super
@@ -28,6 +30,7 @@ module Apartment
28
30
  end
29
31
  end
30
32
 
33
+ # Mysql2 Schemas Adapter
31
34
  class Mysql2SchemaAdapter < AbstractAdapter
32
35
  def initialize(config)
33
36
  super
@@ -97,7 +97,23 @@ module Apartment
97
97
  end
98
98
 
99
99
  def create_tenant_command(conn, tenant)
100
- conn.execute(%(CREATE SCHEMA "#{tenant}"))
100
+ # NOTE: This was causing some tests to fail because of the database strategy for rspec
101
+ if ActiveRecord::Base.connection.open_transactions.positive?
102
+ conn.execute(%(CREATE SCHEMA "#{tenant}"))
103
+ else
104
+ schema = %(BEGIN;
105
+ CREATE SCHEMA "#{tenant}";
106
+ COMMIT;)
107
+
108
+ conn.execute(schema)
109
+ end
110
+ rescue *rescuable_exceptions => e
111
+ rollback_transaction(conn)
112
+ raise e
113
+ end
114
+
115
+ def rollback_transaction(conn)
116
+ conn.execute('ROLLBACK;')
101
117
  end
102
118
 
103
119
  # Generate the final search path to set including persistent_schemas
@@ -121,12 +137,17 @@ module Apartment
121
137
  # There is `reset_sequence_name`, but that method actually goes to the database
122
138
  # to find out the new name. Therefore, we do this hack to only unset the name,
123
139
  # and it will be dynamically found the next time it is needed
124
- ActiveRecord::Base.descendants
125
- .select { |c| c.instance_variable_defined?(:@sequence_name) }
126
- .reject { |c| c.instance_variable_defined?(:@explicit_sequence_name) && c.instance_variable_get(:@explicit_sequence_name) }
127
- .each do |c|
128
- c.remove_instance_variable :@sequence_name
129
- end
140
+ descendants_to_unset = ActiveRecord::Base.descendants
141
+ .select { |c| c.instance_variable_defined?(:@sequence_name) }
142
+ .reject do |c|
143
+ c.instance_variable_defined?(:@explicit_sequence_name) &&
144
+ c.instance_variable_get(:@explicit_sequence_name)
145
+ end
146
+ descendants_to_unset.each do |c|
147
+ # NOTE: due to this https://github.com/rails-on-services/apartment/issues/81
148
+ # unreproduceable error we're checking before trying to remove it
149
+ c.remove_instance_variable :@sequence_name if c.instance_variable_defined?(:@sequence_name)
150
+ end
130
151
  end
131
152
  end
132
153
 
@@ -195,9 +216,11 @@ module Apartment
195
216
  #
196
217
  # @return {String} raw SQL contaning inserts with data from schema_migrations
197
218
  #
219
+ # rubocop:disable Layout/LineLength
198
220
  def pg_dump_schema_migrations_data
199
221
  with_pg_env { `pg_dump -a --inserts -t #{default_tenant}.schema_migrations -t #{default_tenant}.ar_internal_metadata #{dbname}` }
200
222
  end
223
+ # rubocop:enable Layout/LineLength
201
224
 
202
225
  # Temporary set Postgresql related environment variables if there are in @config
203
226
  #
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A workaraound to get `reload!` to also call Apartment::Tenant.init
3
+ # A workaround to get `reload!` to also call Apartment::Tenant.init
4
4
  # This is unfortunate, but I haven't figured out how to hook into the reload process *after* files are reloaded
5
5
 
6
6
  # reloads the environment
7
+ # rubocop:disable Style/OptionalBooleanParameter
7
8
  def reload!(print = true)
8
- # rubocop:disable Rails/Output
9
9
  puts 'Reloading...' if print
10
- # rubocop:enable Rails/Output
11
10
 
12
11
  # This triggers the to_prepare callbacks
13
12
  ActionDispatch::Callbacks.new(proc {}).call({})
@@ -15,18 +14,17 @@ def reload!(print = true)
15
14
  Apartment::Tenant.init
16
15
  true
17
16
  end
17
+ # rubocop:enable Style/OptionalBooleanParameter
18
18
 
19
19
  def st(schema_name = nil)
20
20
  if schema_name.nil?
21
- # rubocop:disable Rails/Output
22
21
  tenant_list.each { |t| puts t }
23
- # rubocop:enable Rails/Output
22
+
24
23
  elsif tenant_list.include? schema_name
25
24
  Apartment::Tenant.switch!(schema_name)
26
25
  else
27
- # rubocop:disable Rails/Output
28
26
  puts "Tenant #{schema_name} is not part of the tenant list"
29
- # rubocop:enable Rails/Output
27
+
30
28
  end
31
29
  end
32
30
 
@@ -37,8 +35,6 @@ def tenant_list
37
35
  end
38
36
 
39
37
  def tenant_info_msg
40
- # rubocop:disable Rails/Output
41
38
  puts "Available Tenants: #{tenant_list}\n"
42
39
  puts "Use `st 'tenant'` to switch tenants & `tenant_list` to see list\n"
43
- # rubocop:enable Rails/Output
44
40
  end
@@ -7,9 +7,9 @@ module Apartment
7
7
  begin
8
8
  require 'pry-rails'
9
9
  rescue LoadError
10
- # rubocop:disable Rails/Output
10
+ # rubocop:disable Layout/LineLength
11
11
  puts '[Failed to load pry-rails] If you want to use Apartment custom prompt you need to add pry-rails to your gemfile'
12
- # rubocop:enable Rails/Output
12
+ # rubocop:enable Layout/LineLength
13
13
  end
14
14
 
15
15
  desc = "Includes the current Rails environment and project folder name.\n" \
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/log_subscriber'
4
+
5
+ module Apartment
6
+ # Custom Log subscriber to include database name and schema name in sql logs
7
+ class LogSubscriber < ActiveRecord::LogSubscriber
8
+ # NOTE: for some reason, if the method definition is not here, then the custom debug method is not called
9
+ # rubocop:disable Lint/UselessMethodDefinition
10
+ def sql(event)
11
+ super(event)
12
+ end
13
+ # rubocop:enable Lint/UselessMethodDefinition
14
+
15
+ private
16
+
17
+ def debug(progname = nil, &block)
18
+ progname = " #{apartment_log}#{progname}" unless progname.nil?
19
+
20
+ super(progname, &block)
21
+ end
22
+
23
+ def apartment_log
24
+ database = color("[#{Apartment.connection.raw_connection.db}] ", ActiveSupport::LogSubscriber::MAGENTA, true)
25
+ schema = nil
26
+ unless Apartment.connection.schema_search_path.nil?
27
+ schema = color("[#{Apartment.connection.schema_search_path.tr('"', '')}] ",
28
+ ActiveSupport::LogSubscriber::YELLOW, true)
29
+ end
30
+ "#{database}#{schema}"
31
+ end
32
+ end
33
+ end
@@ -25,28 +25,33 @@ module Apartment
25
25
  ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
26
26
  end
27
27
 
28
- # Make sure Apartment is reconfigured when the code is reloaded
28
+ # Hook into ActionDispatch::Reloader to ensure Apartment is properly initialized
29
+ # Note that this doesn't entirely work as expected in Development,
30
+ # because this is called before classes are reloaded
31
+ # See the middleware/console declarations below to help with this. Hope to fix that soon.
32
+ #
29
33
  config.to_prepare do
30
- Apartment::Tenant.reinitialize
31
- end
34
+ next if ARGV.any? { |arg| arg =~ /\Aassets:(?:precompile|clean)\z/ }
35
+ next if ARGV.any? { |arg| arg == 'webpacker:compile' }
36
+ next if ENV['APARTMENT_DISABLE_INIT']
32
37
 
33
- #
34
- # Ensure that Apartment::Tenant.init is called when
35
- # a new connection is requested.
36
- #
37
- module ApartmentInitializer
38
- def connection
39
- super.tap do
40
- Apartment::Tenant.init_once
38
+ begin
39
+ Apartment.connection_class.connection_pool.with_connection do
40
+ Apartment::Tenant.init
41
41
  end
42
+ rescue ::ActiveRecord::NoDatabaseError
43
+ # Since `db:create` and other tasks invoke this block from Rails 5.2.0,
44
+ # we need to swallow the error to execute `db:create` properly.
42
45
  end
46
+ end
43
47
 
44
- def arel_table
45
- Apartment::Tenant.init_once
46
- super
48
+ config.after_initialize do
49
+ # NOTE: Load the custom log subscriber if enabled
50
+ if Apartment.active_record_log
51
+ ActiveSupport::Notifications.unsubscribe 'sql.active_record'
52
+ Apartment::LogSubscriber.attach_to :active_record
47
53
  end
48
54
  end
49
- ActiveRecord::Base.singleton_class.prepend ApartmentInitializer
50
55
 
51
56
  #
52
57
  # Ensure rake tasks are loaded
@@ -57,10 +62,8 @@ module Apartment
57
62
  end
58
63
 
59
64
  #
60
- # The following initializers are a workaround to the fact that I can't
61
- # properly hook into the rails reloader
62
- # Note this is technically valid for any environment where cache_classes
63
- # is false, for us, it's just development
65
+ # The following initializers are a workaround to the fact that I can't properly hook into the rails reloader
66
+ # Note this is technically valid for any environment where cache_classes is false, for us, it's just development
64
67
  #
65
68
  if Rails.env.development?
66
69
 
@@ -69,11 +72,13 @@ module Apartment
69
72
  app.config.middleware.use Apartment::Reloader
70
73
  end
71
74
 
72
- # Overrides reload! to also call Apartment::Tenant.init as well so that the
73
- # reloaded classes have the proper table_names
75
+ # Overrides reload! to also call Apartment::Tenant.init as well
76
+ # so that the reloaded classes have the proper table_names
77
+ # rubocop:disable Lint/Debugger
74
78
  console do
75
79
  require 'apartment/console'
76
80
  end
81
+ # rubocop:enable Lint/Debugger
77
82
  end
78
83
  end
79
84
  end