activerecord-tenanted 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_record/tenanted/connection_adapter.rb +15 -6
- data/lib/active_record/tenanted/console.rb +5 -1
- data/lib/active_record/tenanted/database_adapter.rb +30 -0
- data/lib/active_record/tenanted/database_adapters/sqlite.rb +115 -0
- data/lib/active_record/tenanted/database_configurations/base_config.rb +10 -49
- data/lib/active_record/tenanted/database_configurations/tenant_config.rb +14 -16
- data/lib/active_record/tenanted/database_tasks.rb +80 -33
- data/lib/active_record/tenanted/railtie.rb +4 -0
- data/lib/active_record/tenanted/tenant.rb +18 -24
- data/lib/active_record/tenanted/untenanted_connection_pool.rb +4 -0
- data/lib/active_record/tenanted/version.rb +1 -1
- data/lib/active_record/tenanted.rb +15 -0
- data/lib/tasks/active_record/tenanted_tasks.rake +13 -71
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62c3b9d7f37db44de60c7559e4b10e1407171ab29aa874e2687853e8acc38bb1
|
|
4
|
+
data.tar.gz: 27b2ee871ff4ce67b60480e83694cf6a2aeac504b3ab39f96ea0eb243b5f3a47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7eff54730cbc865dd8ecb1169e146194b0edceaed0527059ed110537aedd57901a5a5af5b85c522bfebcf50714c468984f91ac4f5c8e40077b2e737497eac550
|
|
7
|
+
data.tar.gz: fafa6f206173a7057e090407f5cf6698bd7f1d34885509793568f1e76baedbe61b7eeea980493b5af0df30c3f5bd20400c799eebecf62785f458e925afbee57f
|
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveRecord
|
|
4
4
|
module Tenanted
|
|
5
|
-
|
|
5
|
+
#
|
|
6
|
+
# Extends ActiveRecord::ConnectionAdapters::AbstractAdapter with a `tenant` attribute.
|
|
7
|
+
#
|
|
8
|
+
# This is useful in conjunction with the `:tenant` query log tag, which configures logging of
|
|
9
|
+
# the tenant in SQL query logs (when `config.active_record.query_log_tags_enabled` is set to
|
|
10
|
+
# `true`). For example:
|
|
11
|
+
#
|
|
12
|
+
# Rails.application.config.active_record.query_log_tags_enabled = true
|
|
13
|
+
# Rails.application.config.active_record.query_log_tags = [ :tenant ]
|
|
14
|
+
#
|
|
15
|
+
# will cause the application to emit logs like:
|
|
16
|
+
#
|
|
17
|
+
# User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 /*tenant='foo'*/
|
|
18
|
+
#
|
|
19
|
+
module ConnectionAdapter
|
|
6
20
|
extend ActiveSupport::Concern
|
|
7
21
|
|
|
8
22
|
prepended do
|
|
9
23
|
attr_accessor :tenant
|
|
10
24
|
end
|
|
11
25
|
|
|
12
|
-
def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, allow_retry: false, &block)
|
|
13
|
-
name = [ name, "[tenant=#{tenant}]" ].compact.join(" ") if tenanted?
|
|
14
|
-
super
|
|
15
|
-
end
|
|
16
|
-
|
|
17
26
|
def tenanted?
|
|
18
27
|
tenant.present?
|
|
19
28
|
end
|
|
@@ -5,7 +5,11 @@ module ActiveRecord
|
|
|
5
5
|
module Console # :nodoc:
|
|
6
6
|
module IRBConsole
|
|
7
7
|
def start
|
|
8
|
-
|
|
8
|
+
# TODO: we could be setting the current tenant for all tenanted configs.
|
|
9
|
+
if Rails.env.local? && ActiveRecord::Tenanted.connection_class
|
|
10
|
+
config = ActiveRecord::Tenanted.connection_class.connection_pool.db_config
|
|
11
|
+
ActiveRecord::Tenanted::DatabaseTasks.new(config).set_current_tenant
|
|
12
|
+
end
|
|
9
13
|
super
|
|
10
14
|
end
|
|
11
15
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Tenanted
|
|
5
|
+
module DatabaseAdapter # :nodoc:
|
|
6
|
+
# Hash of registered database configuration adapters
|
|
7
|
+
@adapters = {}
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def register(name, class_name)
|
|
11
|
+
@adapters[name.to_s] = class_name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def new(db_config)
|
|
15
|
+
adapter_class_name = @adapters[db_config.adapter]
|
|
16
|
+
|
|
17
|
+
if adapter_class_name.nil?
|
|
18
|
+
raise ActiveRecord::Tenanted::UnsupportedDatabaseError,
|
|
19
|
+
"Unsupported database adapter for tenanting: #{db_config.adapter}. " \
|
|
20
|
+
"Supported adapters: #{@adapters.keys.join(', ')}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
adapter_class_name.constantize.new(db_config)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
register "sqlite3", "ActiveRecord::Tenanted::DatabaseAdapters::SQLite"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module Tenanted
|
|
5
|
+
module DatabaseAdapters # :nodoc:
|
|
6
|
+
#
|
|
7
|
+
# TODO: This still feels to me like it's not _quite_ right. I think we could further refactor this by:
|
|
8
|
+
#
|
|
9
|
+
# 1. Moving tenant_databases and validate_tenant_name to BaseConfig, and subclassing it for
|
|
10
|
+
# each database
|
|
11
|
+
# 2. Moving create_database, drop_database, database_exist?, database_ready?,
|
|
12
|
+
# acquire_ready_lock, ensure_database_directory_exists, and database_path to the SQLite
|
|
13
|
+
# connection adapter, possibly into Rails
|
|
14
|
+
# 3. Moving test_workerize and path_for to be SQLite connection adapter class methods,
|
|
15
|
+
# possibly into Rails
|
|
16
|
+
#
|
|
17
|
+
class SQLite
|
|
18
|
+
attr_reader :db_config
|
|
19
|
+
|
|
20
|
+
def initialize(db_config)
|
|
21
|
+
@db_config = db_config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tenant_databases
|
|
25
|
+
glob = path_for(db_config.database_for("*"))
|
|
26
|
+
scanner = Regexp.new(path_for(db_config.database_for("(.+)")))
|
|
27
|
+
|
|
28
|
+
Dir.glob(glob).filter_map do |path|
|
|
29
|
+
result = path.scan(scanner).flatten.first
|
|
30
|
+
if result.nil?
|
|
31
|
+
Rails.logger.warn "ActiveRecord::Tenanted: Cannot parse tenant name from filename #{path.inspect}"
|
|
32
|
+
end
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_tenant_name(tenant_name)
|
|
38
|
+
if tenant_name.match?(%r{[/'"`]})
|
|
39
|
+
raise BadTenantNameError, "Tenant name contains an invalid character: #{tenant_name.inspect}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_database
|
|
44
|
+
ensure_database_directory_exists
|
|
45
|
+
FileUtils.touch(database_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def drop_database
|
|
49
|
+
# Remove the SQLite database file and associated files
|
|
50
|
+
FileUtils.rm_f(database_path)
|
|
51
|
+
FileUtils.rm_f("#{database_path}-wal") # Write-Ahead Logging file
|
|
52
|
+
FileUtils.rm_f("#{database_path}-shm") # Shared Memory file
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def database_exist?
|
|
56
|
+
File.exist?(database_path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def database_ready?
|
|
60
|
+
File.exist?(database_path) && !ActiveRecord::Tenanted::Mutex::Ready.locked?(database_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def acquire_ready_lock(&block)
|
|
64
|
+
ActiveRecord::Tenanted::Mutex::Ready.lock(database_path, &block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ensure_database_directory_exists
|
|
68
|
+
return unless database_path
|
|
69
|
+
|
|
70
|
+
database_dir = File.dirname(database_path)
|
|
71
|
+
unless File.directory?(database_dir)
|
|
72
|
+
FileUtils.mkdir_p(database_dir)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def database_path
|
|
77
|
+
path_for(db_config.database)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_workerize(db, test_worker_id)
|
|
81
|
+
test_worker_suffix = "_#{test_worker_id}"
|
|
82
|
+
|
|
83
|
+
if db.start_with?("file:") && db.include?("?")
|
|
84
|
+
db.sub(/(\?.*)$/, "#{test_worker_suffix}\\1")
|
|
85
|
+
else
|
|
86
|
+
# This check is needed because of https://github.com/rails/rails/pull/55769 adding
|
|
87
|
+
# replicas to the parallelization setup by using `include_hidden: true` which pulls in
|
|
88
|
+
# the BaseConfig. We don't want to double-suffix the database name.
|
|
89
|
+
#
|
|
90
|
+
# TODO: Ideally we should have finer-grained filtering of database configurations in Rails
|
|
91
|
+
# (other than simply hidden or not-hidden).
|
|
92
|
+
if db.end_with?(test_worker_suffix)
|
|
93
|
+
db
|
|
94
|
+
else
|
|
95
|
+
db + test_worker_suffix
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A sqlite database path can be a file path or a URI (either relative or absolute). We
|
|
101
|
+
# can't parse it as a standard URI in all circumstances, though, see
|
|
102
|
+
# https://sqlite.org/uri.html
|
|
103
|
+
def path_for(database)
|
|
104
|
+
if database.start_with?("file:/")
|
|
105
|
+
URI.parse(database).path
|
|
106
|
+
elsif database.start_with?("file:")
|
|
107
|
+
URI.parse(database.sub(/\?.*$/, "")).opaque
|
|
108
|
+
else
|
|
109
|
+
database
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -11,6 +11,11 @@ module ActiveRecord
|
|
|
11
11
|
def initialize(...)
|
|
12
12
|
super
|
|
13
13
|
@test_worker_id = nil
|
|
14
|
+
@config_adapter = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def config_adapter
|
|
18
|
+
@config_adapter ||= ActiveRecord::Tenanted::DatabaseAdapter.new(self)
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
def database_tasks?
|
|
@@ -20,33 +25,19 @@ module ActiveRecord
|
|
|
20
25
|
def database_for(tenant_name)
|
|
21
26
|
tenant_name = tenant_name.to_s
|
|
22
27
|
|
|
23
|
-
validate_tenant_name(tenant_name)
|
|
28
|
+
config_adapter.validate_tenant_name(tenant_name)
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
db = sprintf(database, tenant: tenant_name)
|
|
26
31
|
|
|
27
32
|
if test_worker_id
|
|
28
|
-
|
|
29
|
-
else
|
|
30
|
-
path
|
|
33
|
+
db = config_adapter.test_workerize(db, test_worker_id)
|
|
31
34
|
end
|
|
32
|
-
end
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
coerce_path(database_for(tenant_name))
|
|
36
|
+
db
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def tenants
|
|
39
|
-
|
|
40
|
-
scanner = Regexp.new(database_path_for("(.+)"))
|
|
41
|
-
|
|
42
|
-
Dir.glob(glob).map do |path|
|
|
43
|
-
result = path.scan(scanner).flatten.first
|
|
44
|
-
if result.nil?
|
|
45
|
-
warn "WARN: ActiveRecord::Tenanted: Cannot parse tenant name from filename #{path.inspect}. " \
|
|
46
|
-
"This is a bug, please report it to https://github.com/basecamp/activerecord-tenanted/issues"
|
|
47
|
-
end
|
|
48
|
-
result
|
|
49
|
-
end
|
|
40
|
+
config_adapter.tenant_databases
|
|
50
41
|
end
|
|
51
42
|
|
|
52
43
|
def new_tenant_config(tenant_name)
|
|
@@ -54,7 +45,6 @@ module ActiveRecord
|
|
|
54
45
|
config_hash = configuration_hash.dup.tap do |hash|
|
|
55
46
|
hash[:tenant] = tenant_name
|
|
56
47
|
hash[:database] = database_for(tenant_name)
|
|
57
|
-
hash[:database_path] = database_path_for(tenant_name)
|
|
58
48
|
hash[:tenanted_config_name] = name
|
|
59
49
|
end
|
|
60
50
|
Tenanted::DatabaseConfigurations::TenantConfig.new(env_name, config_name, config_hash)
|
|
@@ -70,35 +60,6 @@ module ActiveRecord
|
|
|
70
60
|
def max_connection_pools
|
|
71
61
|
(configuration_hash[:max_connection_pools] || DEFAULT_MAX_CONNECTION_POOLS).to_i
|
|
72
62
|
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
# A sqlite database path can be a file path or a URI (either relative or absolute).
|
|
76
|
-
# We can't parse it as a standard URI in all circumstances, though, see https://sqlite.org/uri.html
|
|
77
|
-
def coerce_path(path)
|
|
78
|
-
if path.start_with?("file:/")
|
|
79
|
-
URI.parse(path).path
|
|
80
|
-
elsif path.start_with?("file:")
|
|
81
|
-
URI.parse(path.sub(/\?.*$/, "")).opaque
|
|
82
|
-
else
|
|
83
|
-
path
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def validate_tenant_name(tenant_name)
|
|
88
|
-
if tenant_name.match?(%r{[/'"`]})
|
|
89
|
-
raise BadTenantNameError, "Tenant name contains an invalid character: #{tenant_name.inspect}"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def test_worker_path(path)
|
|
94
|
-
test_worker_suffix = "_#{test_worker_id}"
|
|
95
|
-
|
|
96
|
-
if path.start_with?("file:") && path.include?("?")
|
|
97
|
-
path.sub(/(\?.*)$/, "#{test_worker_suffix}\\1")
|
|
98
|
-
else
|
|
99
|
-
path + test_worker_suffix
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
63
|
end
|
|
103
64
|
end
|
|
104
65
|
end
|
|
@@ -4,13 +4,25 @@ module ActiveRecord
|
|
|
4
4
|
module Tenanted
|
|
5
5
|
module DatabaseConfigurations
|
|
6
6
|
class TenantConfig < ActiveRecord::DatabaseConfigurations::HashConfig
|
|
7
|
+
def initialize(...)
|
|
8
|
+
super
|
|
9
|
+
@config_adapter = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
def tenant
|
|
8
13
|
configuration_hash.fetch(:tenant)
|
|
9
14
|
end
|
|
10
15
|
|
|
16
|
+
def config_adapter
|
|
17
|
+
@config_adapter ||= ActiveRecord::Tenanted::DatabaseAdapter.new(self)
|
|
18
|
+
end
|
|
19
|
+
|
|
11
20
|
def new_connection
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
# TODO: This line can be removed once rails/rails@f1f60dc1 is in a released version of
|
|
22
|
+
# Rails, and this gem's dependency has been bumped to require that version or later.
|
|
23
|
+
config_adapter.ensure_database_directory_exists
|
|
24
|
+
|
|
25
|
+
super.tap { |connection| connection.tenant = tenant }
|
|
14
26
|
end
|
|
15
27
|
|
|
16
28
|
def tenanted_config_name
|
|
@@ -36,20 +48,6 @@ module ActiveRecord
|
|
|
36
48
|
File.join(db_dir, "#{tenanted_config_name}_schema_cache.yml")
|
|
37
49
|
end
|
|
38
50
|
end
|
|
39
|
-
|
|
40
|
-
def database_path
|
|
41
|
-
configuration_hash[:database_path]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
def ensure_database_directory_exists
|
|
46
|
-
return unless database_path
|
|
47
|
-
|
|
48
|
-
database_dir = File.dirname(database_path)
|
|
49
|
-
unless File.directory?(database_dir)
|
|
50
|
-
FileUtils.mkdir_p(database_dir)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
end
|
|
@@ -1,58 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rake"
|
|
4
|
+
|
|
3
5
|
module ActiveRecord
|
|
4
6
|
module Tenanted
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def migrate_all
|
|
9
|
-
raise ArgumentError, "Could not find a tenanted database" unless root_config = root_database_config
|
|
7
|
+
class DatabaseTasks # :nodoc:
|
|
8
|
+
include Rake::DSL
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
migrate(tenant_config)
|
|
10
|
+
class << self
|
|
11
|
+
def verbose?
|
|
12
|
+
ActiveRecord::Tasks::DatabaseTasks.send(:verbose?)
|
|
15
13
|
end
|
|
16
14
|
end
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
attr_reader :config
|
|
17
|
+
|
|
18
|
+
def initialize(config)
|
|
19
|
+
unless config.is_a?(ActiveRecord::Tenanted::DatabaseConfigurations::BaseConfig)
|
|
20
|
+
raise TypeError, "Argument must be an instance of ActiveRecord::Tenanted::DatabaseConfigurations::BaseConfig"
|
|
21
|
+
end
|
|
22
|
+
@config = config
|
|
23
|
+
end
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
def migrate_all
|
|
26
|
+
tenants.each do |tenant|
|
|
27
|
+
migrate_tenant(tenant)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
def migrate_tenant(tenant = set_current_tenant)
|
|
32
|
+
db_config = config.new_tenant_config(tenant)
|
|
33
|
+
migrate(db_config)
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
def drop_all
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
root_config.tenants.each do |tenant|
|
|
30
|
-
# NOTE: This is obviously a sqlite-specific implementation.
|
|
31
|
-
# TODO: Create a `drop_database` method upstream in the sqlite3 adapter, and call it.
|
|
32
|
-
# Then this would delegate to the adapter and become adapter-agnostic.
|
|
33
|
-
root_config.database_path_for(tenant).tap do |path|
|
|
34
|
-
FileUtils.rm(path)
|
|
35
|
-
$stdout.puts "Dropped database '#{path}'" if verbose?
|
|
36
|
-
end
|
|
37
|
+
tenants.each do |tenant|
|
|
38
|
+
drop_tenant(tenant)
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
def drop_tenant(tenant = set_current_tenant)
|
|
43
|
+
db_config = config.new_tenant_config(tenant)
|
|
44
|
+
db_config.config_adapter.drop_database
|
|
45
|
+
$stdout.puts "Dropped database '#{db_config.database}'" if verbose?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tenants
|
|
49
|
+
config.tenants.presence || [ get_default_tenant ].compact
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
def
|
|
52
|
+
def get_default_tenant
|
|
53
|
+
# TODO: needs to work with multiple tenanted configs, maybe using ENV["ARTENANT_#{config.name}"]
|
|
49
54
|
tenant = ENV["ARTENANT"]
|
|
50
55
|
|
|
51
56
|
if tenant.present?
|
|
52
57
|
$stdout.puts "Setting current tenant to #{tenant.inspect}" if verbose?
|
|
53
58
|
elsif Rails.env.local?
|
|
54
59
|
tenant = Rails.application.config.active_record_tenanted.default_tenant
|
|
55
|
-
$stdout.puts "Defaulting current tenant to #{tenant.inspect}" if verbose?
|
|
60
|
+
$stdout.puts "Defaulting current tenant for #{config.name.inspect} to #{tenant.inspect}" if verbose?
|
|
56
61
|
else
|
|
57
62
|
tenant = nil
|
|
58
63
|
$stdout.puts "Cannot determine an implicit tenant: ARTENANT not set, and Rails.env is not local." if verbose?
|
|
@@ -68,7 +73,7 @@ module ActiveRecord
|
|
|
68
73
|
end
|
|
69
74
|
|
|
70
75
|
if connection_class.current_tenant.nil?
|
|
71
|
-
connection_class.current_tenant =
|
|
76
|
+
connection_class.current_tenant = get_default_tenant
|
|
72
77
|
else
|
|
73
78
|
connection_class.current_tenant
|
|
74
79
|
end
|
|
@@ -111,7 +116,49 @@ module ActiveRecord
|
|
|
111
116
|
end
|
|
112
117
|
|
|
113
118
|
def verbose?
|
|
114
|
-
|
|
119
|
+
self.class.verbose?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def register_rake_tasks
|
|
123
|
+
name = config.name
|
|
124
|
+
|
|
125
|
+
desc "Migrate tenanted #{name} databases for current environment"
|
|
126
|
+
task "db:migrate:#{name}" => "load_config" do
|
|
127
|
+
verbose_was = ActiveRecord::Migration.verbose
|
|
128
|
+
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
|
|
129
|
+
|
|
130
|
+
tenant = ENV["ARTENANT"]
|
|
131
|
+
if tenant.present?
|
|
132
|
+
migrate_tenant(tenant)
|
|
133
|
+
else
|
|
134
|
+
migrate_all
|
|
135
|
+
end
|
|
136
|
+
ensure
|
|
137
|
+
ActiveRecord::Migration.verbose = verbose_was
|
|
138
|
+
end
|
|
139
|
+
task "db:migrate" => "db:migrate:#{name}"
|
|
140
|
+
task "db:prepare" => "db:migrate:#{name}"
|
|
141
|
+
|
|
142
|
+
desc "Drop tenanted #{name} databases for current environment"
|
|
143
|
+
task "db:drop:#{name}" => "load_config" do
|
|
144
|
+
verbose_was = ActiveRecord::Migration.verbose
|
|
145
|
+
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
|
|
146
|
+
|
|
147
|
+
tenant = ENV["ARTENANT"]
|
|
148
|
+
if tenant.present?
|
|
149
|
+
drop_tenant(tenant)
|
|
150
|
+
else
|
|
151
|
+
drop_all
|
|
152
|
+
end
|
|
153
|
+
ensure
|
|
154
|
+
ActiveRecord::Migration.verbose = verbose_was
|
|
155
|
+
end
|
|
156
|
+
task "db:drop" => "db:drop:#{name}"
|
|
157
|
+
|
|
158
|
+
# TODO: Rails' database tasks include "db:seed" in the tasks that "db:reset" runs.
|
|
159
|
+
desc "Drop and recreate tenanted #{name} database from its schema for the current environment"
|
|
160
|
+
task "db:reset:#{name}" => [ "db:drop:#{name}", "db:migrate:#{name}" ]
|
|
161
|
+
task "db:reset" => "db:reset:#{name}"
|
|
115
162
|
end
|
|
116
163
|
end
|
|
117
164
|
end
|
|
@@ -148,6 +148,10 @@ module ActiveRecord
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
config.after_initialize do
|
|
151
|
+
ActiveRecord::QueryLogs.taggings = ActiveRecord::QueryLogs.taggings.merge(
|
|
152
|
+
tenant: ->(context) { context[:connection].tenant }
|
|
153
|
+
)
|
|
154
|
+
|
|
151
155
|
if defined?(Rails::Console)
|
|
152
156
|
require "rails/commands/console/irb_console"
|
|
153
157
|
Rails::Console::IRBConsole.prepend ActiveRecord::Tenanted::Console::IRBConsole
|
|
@@ -13,11 +13,13 @@ module ActiveRecord
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def cache_key
|
|
16
|
-
tenant ? "#{
|
|
16
|
+
tenant ? "#{tenant}/#{super}" : super
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def inspect
|
|
20
|
-
|
|
20
|
+
return super unless tenant
|
|
21
|
+
|
|
22
|
+
super.sub(/\A#<\S+ /, "\\0tenant: #{tenant.inspect}, ")
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def to_global_id(options = {})
|
|
@@ -99,10 +101,7 @@ module ActiveRecord
|
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
def tenant_exist?(tenant_name)
|
|
102
|
-
|
|
103
|
-
database_path = tenanted_root_config.database_path_for(tenant_name)
|
|
104
|
-
|
|
105
|
-
File.exist?(database_path) && !ActiveRecord::Tenanted::Mutex::Ready.locked?(database_path)
|
|
104
|
+
tenanted_root_config.new_tenant_config(tenant_name).config_adapter.database_ready?
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
def with_tenant(tenant_name, prohibit_shard_swapping: true, &block)
|
|
@@ -121,24 +120,22 @@ module ActiveRecord
|
|
|
121
120
|
|
|
122
121
|
def create_tenant(tenant_name, if_not_exists: false, &block)
|
|
123
122
|
created_db = false
|
|
124
|
-
|
|
123
|
+
base_config = tenanted_root_config
|
|
124
|
+
adapter = base_config.new_tenant_config(tenant_name).config_adapter
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
unless
|
|
128
|
-
|
|
129
|
-
# TODO: Add a `create_database` method upstream in the sqlite3 adapter, and call it.
|
|
130
|
-
# Then this would delegate to the adapter and become adapter-agnostic.
|
|
131
|
-
FileUtils.touch(database_path)
|
|
126
|
+
adapter.acquire_ready_lock do
|
|
127
|
+
unless adapter.database_exist?
|
|
128
|
+
adapter.create_database
|
|
132
129
|
|
|
133
130
|
with_tenant(tenant_name) do
|
|
134
131
|
connection_pool(schema_version_check: false)
|
|
135
|
-
ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant(tenant_name)
|
|
132
|
+
ActiveRecord::Tenanted::DatabaseTasks.new(base_config).migrate_tenant(tenant_name)
|
|
136
133
|
end
|
|
137
134
|
|
|
138
135
|
created_db = true
|
|
139
136
|
end
|
|
140
137
|
rescue
|
|
141
|
-
|
|
138
|
+
adapter.drop_database
|
|
142
139
|
raise
|
|
143
140
|
end
|
|
144
141
|
|
|
@@ -158,10 +155,7 @@ module ActiveRecord
|
|
|
158
155
|
end
|
|
159
156
|
end
|
|
160
157
|
|
|
161
|
-
|
|
162
|
-
# TODO: Create a `drop_database` method upstream in the sqlite3 adapter, and call it.
|
|
163
|
-
# Then this would delegate to the adapter and become adapter-agnostic.
|
|
164
|
-
FileUtils.rm_f(tenanted_root_config.database_path_for(tenant_name))
|
|
158
|
+
tenanted_root_config.new_tenant_config(tenant_name).config_adapter.drop_database
|
|
165
159
|
end
|
|
166
160
|
|
|
167
161
|
def tenants
|
|
@@ -211,12 +205,12 @@ module ActiveRecord
|
|
|
211
205
|
return superclass._create_tenanted_pool unless connection_class?
|
|
212
206
|
|
|
213
207
|
tenant = current_tenant
|
|
214
|
-
|
|
215
|
-
raise TenantDoesNotExistError, "The database file for tenant #{tenant.inspect} does not exist."
|
|
216
|
-
end
|
|
208
|
+
db_config = tenanted_root_config.new_tenant_config(tenant)
|
|
217
209
|
|
|
218
|
-
|
|
219
|
-
|
|
210
|
+
unless db_config.config_adapter.database_exist?
|
|
211
|
+
raise TenantDoesNotExistError, "The database for tenant #{tenant.inspect} does not exist."
|
|
212
|
+
end
|
|
213
|
+
pool = establish_connection(db_config)
|
|
220
214
|
|
|
221
215
|
if schema_version_check
|
|
222
216
|
pending_migrations = pool.migration_context.open.pending_migrations
|
|
@@ -28,6 +28,10 @@ module ActiveRecord
|
|
|
28
28
|
ActiveRecord::ConnectionAdapters::BoundSchemaReflection.new(schema_reflection, self)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def size
|
|
32
|
+
db_config.max_connections
|
|
33
|
+
end
|
|
34
|
+
|
|
31
35
|
def lease_connection(...)
|
|
32
36
|
raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
|
|
33
37
|
end
|
|
@@ -4,6 +4,9 @@ require "active_record"
|
|
|
4
4
|
|
|
5
5
|
require "zeitwerk"
|
|
6
6
|
loader = Zeitwerk::Loader.for_gem_extension(ActiveRecord)
|
|
7
|
+
loader.inflector.inflect(
|
|
8
|
+
"sqlite" => "SQLite",
|
|
9
|
+
)
|
|
7
10
|
loader.setup
|
|
8
11
|
|
|
9
12
|
module ActiveRecord
|
|
@@ -35,10 +38,22 @@ module ActiveRecord
|
|
|
35
38
|
# Raised when the Rails integration is being invoked but has not been configured.
|
|
36
39
|
class IntegrationNotConfiguredError < Error; end
|
|
37
40
|
|
|
41
|
+
# Raised when an unsupported database adapter is used.
|
|
42
|
+
class UnsupportedDatabaseError < Error; end
|
|
43
|
+
|
|
44
|
+
# Return the constantized connection class configured in `config.active_record_tenanted.connection_class`,
|
|
45
|
+
# or nil if none is configured.
|
|
38
46
|
def self.connection_class
|
|
39
47
|
# TODO: cache this / speed this up
|
|
40
48
|
Rails.application.config.active_record_tenanted.connection_class&.constantize
|
|
41
49
|
end
|
|
50
|
+
|
|
51
|
+
# Return an Array of the tenanted database configurations.
|
|
52
|
+
def self.base_configs(configurations = ActiveRecord::Base.configurations)
|
|
53
|
+
configurations
|
|
54
|
+
.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env, include_hidden: true)
|
|
55
|
+
.select { |c| c.configuration_hash[:tenanted] }
|
|
56
|
+
end
|
|
42
57
|
end
|
|
43
58
|
end
|
|
44
59
|
|
|
@@ -1,78 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
unless ActiveRecord::Tenanted::DatabaseTasks.root_database_config
|
|
12
|
-
warn "WARNING: No tenanted database found, skipping tenanted migration"
|
|
13
|
-
else
|
|
14
|
-
begin
|
|
15
|
-
verbose_was = ActiveRecord::Migration.verbose
|
|
16
|
-
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
|
|
17
|
-
|
|
18
|
-
ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant
|
|
19
|
-
ensure
|
|
20
|
-
ActiveRecord::Migration.verbose = verbose_was
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
desc "Migrate the database for all existing tenants"
|
|
26
|
-
task "migrate:tenant:all" => "load_config" do
|
|
27
|
-
unless ActiveRecord::Tenanted.connection_class
|
|
28
|
-
warn "ActiveRecord::Tenanted integration is not configured via connection_class"
|
|
29
|
-
next
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
verbose_was = ActiveRecord::Migration.verbose
|
|
33
|
-
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
|
|
34
|
-
|
|
35
|
-
ActiveRecord::Tenanted::DatabaseTasks.migrate_all
|
|
36
|
-
ensure
|
|
37
|
-
ActiveRecord::Migration.verbose = verbose_was
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
desc "Drop and recreate all tenant databases from their schema for the current environment"
|
|
41
|
-
task "reset:tenant" => [ "db:drop:tenant", "db:migrate:tenant" ]
|
|
42
|
-
|
|
43
|
-
desc "Drop all tenanted databases for the current environment"
|
|
44
|
-
task "drop:tenant" => "load_config" do
|
|
45
|
-
unless ActiveRecord::Tenanted::DatabaseTasks.root_database_config
|
|
46
|
-
warn "WARNING: No tenanted database found, skipping tenanted reset"
|
|
47
|
-
else
|
|
48
|
-
begin
|
|
49
|
-
verbose_was = ActiveRecord::Migration.verbose
|
|
50
|
-
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
|
|
51
|
-
|
|
52
|
-
ActiveRecord::Tenanted::DatabaseTasks.drop_all
|
|
53
|
-
ensure
|
|
54
|
-
ActiveRecord::Migration.verbose = verbose_was
|
|
55
|
-
end
|
|
56
|
-
end
|
|
3
|
+
# Ensure a default tenant is set for database tasks that may need it.
|
|
4
|
+
desc "Set the current tenant to ARTENANT if present, else the environment default"
|
|
5
|
+
task "db:tenant" => "load_config" do
|
|
6
|
+
unless ActiveRecord::Tenanted.connection_class
|
|
7
|
+
warn "ActiveRecord::Tenanted integration is not configured via connection_class"
|
|
8
|
+
next
|
|
57
9
|
end
|
|
58
10
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
unless ActiveRecord::Tenanted.connection_class
|
|
62
|
-
warn "ActiveRecord::Tenanted integration is not configured via connection_class"
|
|
63
|
-
next
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
ActiveRecord::Tenanted::DatabaseTasks.set_current_tenant
|
|
67
|
-
end
|
|
11
|
+
config = ActiveRecord::Tenanted.connection_class.connection_pool.db_config
|
|
12
|
+
ActiveRecord::Tenanted::DatabaseTasks.new(config).set_current_tenant
|
|
68
13
|
end
|
|
69
|
-
|
|
70
|
-
# Decorate database tasks with the tenanted version.
|
|
71
|
-
task "db:migrate" => "db:migrate:tenant:all"
|
|
72
|
-
task "db:prepare" => "db:migrate:tenant:all"
|
|
73
|
-
task "db:reset" => "db:reset:tenant"
|
|
74
|
-
task "db:drop" => "db:drop:tenant"
|
|
75
|
-
|
|
76
|
-
# Ensure a default tenant is set for database tasks that may need it.
|
|
77
14
|
task "db:fixtures:load" => "db:tenant"
|
|
78
15
|
task "db:seed" => "db:tenant"
|
|
16
|
+
|
|
17
|
+
# Create tenanted rake tasks
|
|
18
|
+
ActiveRecord::Tenanted.base_configs(ActiveRecord::DatabaseConfigurations.new(ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml)).each do |config|
|
|
19
|
+
ActiveRecord::Tenanted::DatabaseTasks.new(config).register_rake_tasks
|
|
20
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-tenanted
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Dalessio
|
|
@@ -73,6 +73,8 @@ files:
|
|
|
73
73
|
- lib/active_record/tenanted/cable_connection.rb
|
|
74
74
|
- lib/active_record/tenanted/connection_adapter.rb
|
|
75
75
|
- lib/active_record/tenanted/console.rb
|
|
76
|
+
- lib/active_record/tenanted/database_adapter.rb
|
|
77
|
+
- lib/active_record/tenanted/database_adapters/sqlite.rb
|
|
76
78
|
- lib/active_record/tenanted/database_configurations.rb
|
|
77
79
|
- lib/active_record/tenanted/database_configurations/base_config.rb
|
|
78
80
|
- lib/active_record/tenanted/database_configurations/tenant_config.rb
|