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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +78 -0
- data/.github/workflows/changelog.yml +63 -0
- data/.rubocop.yml +78 -4
- data/.rubocop_todo.yml +50 -13
- data/CHANGELOG.md +963 -0
- data/Gemfile +2 -2
- data/Guardfile +0 -15
- data/HISTORY.md +93 -92
- data/README.md +27 -0
- data/Rakefile +5 -2
- data/gemfiles/rails_5_0.gemfile +1 -1
- data/gemfiles/rails_5_1.gemfile +1 -1
- data/gemfiles/rails_5_2.gemfile +1 -1
- data/gemfiles/rails_6_0.gemfile +1 -1
- data/gemfiles/rails_master.gemfile +1 -1
- data/lib/apartment.rb +5 -3
- data/lib/apartment/active_record/connection_handling.rb +3 -0
- data/lib/apartment/active_record/internal_metadata.rb +0 -2
- data/lib/apartment/active_record/schema_migration.rb +0 -2
- data/lib/apartment/adapters/abstract_adapter.rb +5 -3
- data/lib/apartment/adapters/abstract_jdbc_adapter.rb +2 -1
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +2 -1
- data/lib/apartment/adapters/mysql2_adapter.rb +3 -0
- data/lib/apartment/adapters/postgresql_adapter.rb +30 -7
- data/lib/apartment/console.rb +5 -9
- data/lib/apartment/custom_console.rb +2 -2
- data/lib/apartment/log_subscriber.rb +33 -0
- data/lib/apartment/railtie.rb +26 -21
- data/lib/apartment/tasks/task_helper.rb +7 -2
- data/lib/apartment/tenant.rb +7 -20
- data/lib/apartment/version.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +2 -1
- data/lib/tasks/apartment.rake +4 -7
- data/ros-apartment.gemspec +2 -2
- metadata +12 -10
- data/.travis.yml +0 -49
- 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
|
-
|
50
|
-
|
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
|
data/gemfiles/rails_5_0.gemfile
CHANGED
data/gemfiles/rails_5_1.gemfile
CHANGED
data/gemfiles/rails_5_2.gemfile
CHANGED
data/gemfiles/rails_6_0.gemfile
CHANGED
data/lib/apartment.rb
CHANGED
@@ -6,16 +6,18 @@ require 'forwardable'
|
|
6
6
|
require 'active_record'
|
7
7
|
require 'apartment/tenant'
|
8
8
|
|
9
|
-
|
9
|
+
require_relative 'apartment/log_subscriber'
|
10
10
|
|
11
|
-
|
12
|
-
require_relative 'apartment/active_record/connection_handling'
|
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
|
-
#
|
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
|
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+)
|
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+)
|
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
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
#
|
data/lib/apartment/console.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# A
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
data/lib/apartment/railtie.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
31
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
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
|
-
#
|
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
|
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
|