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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dbae49297c735850801a4994217ba8ce3ff794345159748c881c6e5e33a6b86
4
- data.tar.gz: 42e0b45e14927d7c403db84baf20499c9297572993d30526522e0ecb810ad87f
3
+ metadata.gz: 62c3b9d7f37db44de60c7559e4b10e1407171ab29aa874e2687853e8acc38bb1
4
+ data.tar.gz: 27b2ee871ff4ce67b60480e83694cf6a2aeac504b3ab39f96ea0eb243b5f3a47
5
5
  SHA512:
6
- metadata.gz: d897569771373793bb56468562b9070d523d1cebe1a2fa1acd7da8e7db40d92f59c8e51439161f33d5c9e3456c9d5797499b2596fc5dcc515b5a4b93959f8122
7
- data.tar.gz: f4edce93a3be90e901251be1b00f788753056c94d4fe0c1df71fcccd0b28aba52f9116c2f144ac94a1b72acb194d44a1e7736df0d971c520088c4a2fd5cddb85
6
+ metadata.gz: 7eff54730cbc865dd8ecb1169e146194b0edceaed0527059ed110537aedd57901a5a5af5b85c522bfebcf50714c468984f91ac4f5c8e40077b2e737497eac550
7
+ data.tar.gz: fafa6f206173a7057e090407f5cf6698bd7f1d34885509793568f1e76baedbe61b7eeea980493b5af0df30c3f5bd20400c799eebecf62785f458e925afbee57f
@@ -2,18 +2,27 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tenanted
5
- module ConnectionAdapter # :nodoc:
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
- ActiveRecord::Tenanted::DatabaseTasks.set_current_tenant if Rails.env.local?
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
- path = sprintf(database, tenant: tenant_name)
30
+ db = sprintf(database, tenant: tenant_name)
26
31
 
27
32
  if test_worker_id
28
- test_worker_path(path)
29
- else
30
- path
33
+ db = config_adapter.test_workerize(db, test_worker_id)
31
34
  end
32
- end
33
35
 
34
- def database_path_for(tenant_name)
35
- coerce_path(database_for(tenant_name))
36
+ db
36
37
  end
37
38
 
38
39
  def tenants
39
- glob = database_path_for("*")
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
- ensure_database_directory_exists # adapter doesn't handle this if the database is a URI
13
- super.tap { |conn| conn.tenant = tenant }
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
- module DatabaseTasks # :nodoc:
6
- extend self
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
- tenants = root_config.tenants.presence || [ get_current_tenant ].compact
12
- tenants.each do |tenant|
13
- tenant_config = root_config.new_tenant_config(tenant)
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
- def migrate_tenant(tenant_name = set_current_tenant)
19
- raise ArgumentError, "Could not find a tenanted database" unless root_config = root_database_config
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
- tenant_config = root_config.new_tenant_config(tenant_name)
25
+ def migrate_all
26
+ tenants.each do |tenant|
27
+ migrate_tenant(tenant)
28
+ end
29
+ end
22
30
 
23
- migrate(tenant_config)
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
- raise ArgumentError, "Could not find a tenanted database" unless root_config = root_database_config
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 root_database_config
41
- db_configs = ActiveRecord::Base.configurations.configs_for(
42
- env_name: ActiveRecord::Tasks::DatabaseTasks.env,
43
- include_hidden: true
44
- )
45
- db_configs.detect { |c| c.configuration_hash[:tenanted] }
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 get_current_tenant
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 = get_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
- ActiveRecord::Tasks::DatabaseTasks.send(:verbose?)
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 ? "#{super}?tenant=#{tenant}" : super
16
+ tenant ? "#{tenant}/#{super}" : super
17
17
  end
18
18
 
19
19
  def inspect
20
- tenant ? super.sub(/>$/, ", tenant=#{tenant.inspect}>") : super
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
- # this will have to be an adapter-specific implementation if we support other than sqlite
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
- database_path = tenanted_root_config.database_path_for(tenant_name)
123
+ base_config = tenanted_root_config
124
+ adapter = base_config.new_tenant_config(tenant_name).config_adapter
125
125
 
126
- ActiveRecord::Tenanted::Mutex::Ready.lock(database_path) do
127
- unless File.exist?(database_path)
128
- # NOTE: This is obviously a sqlite-specific implementation.
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
- FileUtils.rm_f(database_path)
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
- # NOTE: This is obviously a sqlite-specific implementation.
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
- unless File.exist?(tenanted_root_config.database_path_for(tenant))
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
- config = tenanted_root_config.new_tenant_config(tenant)
219
- pool = establish_connection(config)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module Tenanted
5
- VERSION = "0.4.1"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  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
- namespace :db do
4
- desc "Migrate the database for tenant ARTENANT"
5
- task "migrate:tenant" => "load_config" do
6
- unless ActiveRecord::Tenanted.connection_class
7
- warn "ActiveRecord::Tenanted integration is not configured via connection_class"
8
- next
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
- desc "Set the current tenant to ARTENANT if present, else the environment default"
60
- task "tenant" => "load_config" do
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.1
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