activerecord-tenanted 0.1.0 → 0.2.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +46 -21
  4. data/Rakefile +5 -6
  5. data/lib/active_record/tenanted/base.rb +63 -0
  6. data/lib/active_record/tenanted/cable_connection.rb +50 -0
  7. data/lib/active_record/tenanted/connection_adapter.rb +22 -0
  8. data/lib/active_record/tenanted/console.rb +29 -0
  9. data/lib/active_record/tenanted/database_configurations.rb +166 -0
  10. data/lib/active_record/tenanted/database_tasks.rb +118 -0
  11. data/lib/active_record/tenanted/global_id.rb +36 -0
  12. data/lib/active_record/tenanted/job.rb +37 -0
  13. data/lib/active_record/tenanted/mailer.rb +15 -0
  14. data/lib/active_record/tenanted/mutex.rb +66 -0
  15. data/lib/active_record/tenanted/patches.rb +50 -0
  16. data/lib/active_record/tenanted/railtie.rb +201 -0
  17. data/lib/active_record/tenanted/relation.rb +22 -0
  18. data/lib/active_record/tenanted/storage.rb +49 -0
  19. data/lib/active_record/tenanted/subtenant.rb +36 -0
  20. data/lib/active_record/tenanted/tenant.rb +257 -0
  21. data/lib/active_record/tenanted/tenant_selector.rb +57 -0
  22. data/lib/active_record/tenanted/testing.rb +121 -0
  23. data/lib/active_record/tenanted/untenanted_connection_pool.rb +48 -0
  24. data/lib/{activerecord → active_record}/tenanted/version.rb +2 -2
  25. data/lib/active_record/tenanted.rb +47 -0
  26. data/lib/activerecord-tenanted.rb +3 -0
  27. data/lib/tasks/active_record/tenanted_tasks.rake +78 -0
  28. metadata +93 -11
  29. data/CHANGELOG.md +0 -5
  30. data/CODE_OF_CONDUCT.md +0 -132
  31. data/LICENSE.txt +0 -21
  32. data/lib/activerecord/tenanted.rb +0 -10
  33. data/sig/activerecord/tenanted.rbs +0 -6
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tenanted
5
+ # instance methods common to both Tenant and Subtenant
6
+ module TenantCommon # :nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ prepended do
10
+ attr_reader :tenant
11
+
12
+ before_save :ensure_tenant_context_safety
13
+ end
14
+
15
+ def cache_key
16
+ tenant ? "#{super}?tenant=#{tenant}" : super
17
+ end
18
+
19
+ def to_global_id(options = {})
20
+ super(options.merge(tenant: tenant))
21
+ end
22
+
23
+ def to_signed_global_id(options = {})
24
+ super(options.merge(tenant: tenant))
25
+ end
26
+
27
+ def association(name)
28
+ super.tap do |assoc|
29
+ if assoc.reflection.polymorphic? || assoc.reflection.klass.tenanted?
30
+ ensure_tenant_context_safety
31
+ end
32
+ end
33
+ end
34
+
35
+ alias to_gid to_global_id
36
+ alias to_sgid to_signed_global_id
37
+
38
+ private
39
+ # I would prefer to do this in an `after_initialize` callback, but some associations are
40
+ # created before those callbacks are invoked (for example, a `belongs_to` association) and
41
+ # we need to be able to ensure tenant context safety on all associations.
42
+ def init_internals
43
+ @tenant = self.class.current_tenant
44
+ super
45
+ end
46
+
47
+ def ensure_tenant_context_safety
48
+ self_tenant = self.tenant
49
+ current_tenant = self.class.current_tenant
50
+
51
+ if current_tenant.nil?
52
+ raise NoTenantError, "Cannot connect to a tenanted database while untenanted (#{self.class})"
53
+ elsif self_tenant != current_tenant
54
+ raise WrongTenantError,
55
+ "#{self.class} model belongs to tenant #{self_tenant.inspect}, " \
56
+ "but current tenant is #{current_tenant.inspect}"
57
+ end
58
+ end
59
+ end
60
+
61
+ module Tenant
62
+ extend ActiveSupport::Concern
63
+
64
+ # This is a sentinel value used to indicate that the class is not currently tenanted.
65
+ #
66
+ # It's the default value returned by `current_shard` when the class is not tenanted. The
67
+ # `current_tenant` method's job is to recognizes that sentinel value and return `nil`, because
68
+ # Active Record itself does not recognize `nil` as a valid shard value.
69
+ UNTENANTED_SENTINEL = Class.new do # :nodoc:
70
+ def inspect
71
+ "ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL"
72
+ end
73
+
74
+ def to_s
75
+ "(untenanted)"
76
+ end
77
+ end.new.freeze
78
+
79
+ CONNECTION_POOL_CREATION_LOCK = Thread::Mutex.new # :nodoc:
80
+
81
+ class_methods do
82
+ def tenanted?
83
+ true
84
+ end
85
+
86
+ def current_tenant
87
+ shard = current_shard
88
+ shard != UNTENANTED_SENTINEL ? shard : nil
89
+ end
90
+
91
+ def current_tenant=(tenant_name)
92
+ tenant_name = tenant_name.to_s unless tenant_name == UNTENANTED_SENTINEL
93
+
94
+ connection_class_for_self.connecting_to(shard: tenant_name, role: ActiveRecord.writing_role)
95
+ end
96
+
97
+ def tenant_exist?(tenant_name)
98
+ # this will have to be an adapter-specific implementation if we support other than sqlite
99
+ database_path = tenanted_root_config.database_path_for(tenant_name)
100
+
101
+ File.exist?(database_path) && !ActiveRecord::Tenanted::Mutex::Ready.locked?(database_path)
102
+ end
103
+
104
+ def with_tenant(tenant_name, prohibit_shard_swapping: true, &block)
105
+ tenant_name = tenant_name.to_s unless tenant_name == UNTENANTED_SENTINEL
106
+
107
+ if tenant_name == current_tenant
108
+ yield
109
+ else
110
+ connection_class_for_self.connected_to(shard: tenant_name, role: ActiveRecord.writing_role) do
111
+ prohibit_shard_swapping(prohibit_shard_swapping) do
112
+ log_tenant_tag(tenant_name, &block)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def create_tenant(tenant_name, if_not_exists: false, &block)
119
+ created_db = false
120
+ database_path = tenanted_root_config.database_path_for(tenant_name)
121
+
122
+ ActiveRecord::Tenanted::Mutex::Ready.lock(database_path) do
123
+ unless File.exist?(database_path)
124
+ # NOTE: This is obviously a sqlite-specific implementation.
125
+ # TODO: Add a `create_database` method upstream in the sqlite3 adapter, and call it.
126
+ # Then this would delegate to the adapter and become adapter-agnostic.
127
+ FileUtils.touch(database_path)
128
+
129
+ with_tenant(tenant_name) do
130
+ connection_pool(schema_version_check: false)
131
+ ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant(tenant_name)
132
+ end
133
+
134
+ created_db = true
135
+ end
136
+ rescue
137
+ FileUtils.rm_f(database_path)
138
+ raise
139
+ end
140
+
141
+ raise TenantExistsError unless created_db || if_not_exists
142
+
143
+ with_tenant(tenant_name) do
144
+ yield if block_given?
145
+ end
146
+ end
147
+
148
+ def destroy_tenant(tenant_name)
149
+ ActiveRecord::Base.logger.info " DESTROY [tenant=#{tenant_name}] Destroying tenant database"
150
+
151
+ with_tenant(tenant_name, prohibit_shard_swapping: false) do
152
+ if retrieve_connection_pool(strict: false)
153
+ remove_connection
154
+ end
155
+ end
156
+
157
+ # NOTE: This is obviously a sqlite-specific implementation.
158
+ # TODO: Create a `drop_database` method upstream in the sqlite3 adapter, and call it.
159
+ # Then this would delegate to the adapter and become adapter-agnostic.
160
+ FileUtils.rm_f(tenanted_root_config.database_path_for(tenant_name))
161
+ end
162
+
163
+ def tenants
164
+ # DatabaseConfigurations::RootConfig#tenants returns all tenants whose database files
165
+ # exist, but some of those may be getting initially migrated, so we perform an additional
166
+ # filter on readiness with `tenant_exist?`.
167
+ tenanted_root_config.tenants.select { |t| tenant_exist?(t) }
168
+ end
169
+
170
+ def with_each_tenant(**options, &block)
171
+ tenants.each { |tenant| with_tenant(tenant, **options) { yield tenant } }
172
+ end
173
+
174
+ # This method is really only intended to be used for testing.
175
+ def without_tenant(&block) # :nodoc:
176
+ with_tenant(ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL, prohibit_shard_swapping: false, &block)
177
+ end
178
+
179
+ def connection_pool(schema_version_check: true) # :nodoc:
180
+ if current_tenant
181
+ pool = retrieve_connection_pool(strict: false)
182
+
183
+ if pool.nil?
184
+ CONNECTION_POOL_CREATION_LOCK.synchronize do
185
+ # re-check now that we have the lock
186
+ pool = retrieve_connection_pool(strict: false)
187
+
188
+ if pool.nil?
189
+ _create_tenanted_pool(schema_version_check: schema_version_check)
190
+ pool = retrieve_connection_pool(strict: true)
191
+ end
192
+ end
193
+ end
194
+
195
+ pool
196
+ else
197
+ Tenanted::UntenantedConnectionPool.new(tenanted_root_config, self)
198
+ end
199
+ end
200
+
201
+ def tenanted_root_config # :nodoc:
202
+ ActiveRecord::Base.configurations.resolve(tenanted_config_name.to_sym)
203
+ end
204
+
205
+ def tenanted_config_name # :nodoc:
206
+ @tenanted_config_name ||= (superclass.respond_to?(:tenanted_config_name) ? superclass.tenanted_config_name : nil)
207
+ end
208
+
209
+ def _create_tenanted_pool(schema_version_check: true) # :nodoc:
210
+ # ensure all classes use the same connection pool
211
+ return superclass._create_tenanted_pool unless connection_class?
212
+
213
+ 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
217
+
218
+ config = tenanted_root_config.new_tenant_config(tenant)
219
+ pool = establish_connection(config)
220
+
221
+ if schema_version_check
222
+ pending_migrations = pool.migration_context.open.pending_migrations
223
+ raise ActiveRecord::PendingMigrationError.new(pending_migrations: pending_migrations) if pending_migrations.any?
224
+ end
225
+
226
+ pool
227
+ end
228
+
229
+ private
230
+ def retrieve_connection_pool(strict:)
231
+ connection_handler.retrieve_connection_pool(connection_specification_name,
232
+ role: current_role,
233
+ shard: current_tenant,
234
+ strict: strict)
235
+ end
236
+
237
+ def log_tenant_tag(tenant_name, &block)
238
+ if Rails.application.config.active_record_tenanted.log_tenant_tag
239
+ Rails.logger.tagged("tenant=#{tenant_name}", &block)
240
+ else
241
+ yield
242
+ end
243
+ end
244
+ end
245
+
246
+ prepended do
247
+ self.default_shard = ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL
248
+
249
+ prepend TenantCommon
250
+ end
251
+
252
+ def tenanted?
253
+ true
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/contrib"
4
+
5
+ module ActiveRecord
6
+ module Tenanted
7
+ #
8
+ # If config.active_record_tenanted.connection_class is set, this middleware will be loaded
9
+ # automatically, and will use config.active_record_tenanted.tenant_resolver to determine the
10
+ # appropriate tenant for the request.
11
+ #
12
+ # If no tenant is resolved, the request will be executed without wrapping it in a tenanted
13
+ # context. Application code will be free to set the tenant as needed.
14
+ #
15
+ # If a tenant is resolved and the tenant exists, the application will be locked to that
16
+ # tenant's database for the duration of the request.
17
+ #
18
+ # If a tenant is resolved, but the tenant does not exist, a 404 response will be returned.
19
+ #
20
+ class TenantSelector
21
+ attr_reader :app
22
+
23
+ def initialize(app)
24
+ @app = app
25
+ end
26
+
27
+ def call(env)
28
+ request = ActionDispatch::Request.new(env)
29
+ tenant_name = tenant_resolver.call(request)
30
+
31
+ if tenant_name.blank?
32
+ # run the request without wrapping it in a tenanted context
33
+ @app.call(env)
34
+ elsif tenanted_class.tenant_exist?(tenant_name)
35
+ tenanted_class.with_tenant(tenant_name) { @app.call(env) }
36
+ else
37
+ Rails.logger.info("ActiveRecord::Tenanted::TenantSelector: Tenant not found: #{tenant_name.inspect}")
38
+ Rack::NotFound.new(Rails.root.join("public/404.html")).call(env)
39
+ end
40
+ end
41
+
42
+ def tenanted_class
43
+ # Note: we'll probably want to cache this when we look at performance, but don't cache it
44
+ # when class reloading is enabled.
45
+ tenanted_class_name.constantize
46
+ end
47
+
48
+ def tenanted_class_name
49
+ @tenanted_class_name ||= Rails.application.config.active_record_tenanted.connection_class
50
+ end
51
+
52
+ def tenant_resolver
53
+ @tenanted_resolver ||= Rails.application.config.active_record_tenanted.tenant_resolver
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tenanted
5
+ module Testing
6
+ module ActiveSupportTestCase
7
+ extend ActiveSupport::Concern
8
+
9
+ prepended do
10
+ if klass = ActiveRecord::Tenanted.connection_class
11
+ klass.current_tenant = Rails.application.config.active_record_tenanted.default_tenant
12
+ tenant_genesis(klass)
13
+
14
+ parallelize_setup do |worker|
15
+ # free up the connection pool for the tenant name
16
+ klass.destroy_tenant(klass.current_tenant)
17
+
18
+ # destroy and create tenant databases for this worker's unique id
19
+ klass.tenanted_root_config.test_worker_id = worker
20
+ tenant_genesis(klass)
21
+ end
22
+ end
23
+ end
24
+
25
+ class_methods do
26
+ private
27
+ # Destroy any existing tenants from the last test run, and create a fresh tenant
28
+ # database for the current tenant.
29
+ #
30
+ # Yes, this is a Star Trek reference.
31
+ def tenant_genesis(klass)
32
+ klass.tenants.each { |tenant| klass.destroy_tenant(tenant) }
33
+ klass.create_tenant(klass.current_tenant)
34
+ end
35
+ end
36
+ end
37
+
38
+ module ActionDispatchIntegrationTest
39
+ extend ActiveSupport::Concern
40
+
41
+ prepended do
42
+ setup do
43
+ if klass = ActiveRecord::Tenanted.connection_class
44
+ integration_session.host = "#{klass.current_tenant}.example.com"
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ module ActionDispatchIntegrationSession
51
+ def process(...)
52
+ if klass = ActiveRecord::Tenanted.connection_class
53
+ klass.without_tenant { super }
54
+ else
55
+ super
56
+ end
57
+ end
58
+ end
59
+
60
+ module ActionDispatchSystemTestCase
61
+ extend ActiveSupport::Concern
62
+
63
+ prepended do
64
+ setup do
65
+ if klass = ActiveRecord::Tenanted.connection_class
66
+ self.default_url_options = { host: "#{klass.current_tenant}.example.localhost" }
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ module ActiveRecordFixtures
73
+ def transactional_tests_for_pool?(pool)
74
+ config = pool.db_config
75
+
76
+ # Prevent the tenanted RootConfig from creating transactional fixtures on an unnecessary
77
+ # database, which would result in sporadic locking errors.
78
+ is_root_config = config.instance_of?(Tenanted::DatabaseConfigurations::RootConfig)
79
+
80
+ # Any tenanted database that isn't the default test fixture database should not be wrapped
81
+ # in a transaction, for a couple of reasons:
82
+ #
83
+ # 1. we migrate the database using a temporary pool, which will wrap the schema load in a
84
+ # transaction that will not be visible to any connection used by the code under test to
85
+ # insert data.
86
+ # 2. having an open transaction will prevent the test from being able to destroy the tenant.
87
+ is_non_default_tenant = (
88
+ config.instance_of?(Tenanted::DatabaseConfigurations::TenantConfig) &&
89
+ config.tenant != Rails.application.config.active_record_tenanted.default_tenant.to_s
90
+ )
91
+
92
+ return false if is_root_config || is_non_default_tenant
93
+
94
+ super
95
+ end
96
+ end
97
+
98
+ module ActiveJobTestCase
99
+ def perform_enqueued_jobs(...)
100
+ if klass = ActiveRecord::Tenanted.connection_class
101
+ klass.without_tenant { super }
102
+ else
103
+ super
104
+ end
105
+ end
106
+ end
107
+
108
+ module ActionCableTestCase
109
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
110
+ if (klass = ActiveRecord::Tenanted.connection_class) && klass.current_tenant
111
+ env = request_params.fetch(:env, {})
112
+ env["HTTP_HOST"] ||= "#{klass.current_tenant}.example.com"
113
+ request_params[:env] = env
114
+ end
115
+
116
+ super
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tenanted
5
+ # In an untenanted context, instances of this class are returned by `Tenant.connection_pool`.
6
+ #
7
+ # Many places in Rails assume that `.connection_pool` can be called and will return an object,
8
+ # and so we can't just raise an exception if it's called while untenanted.
9
+ #
10
+ # Instead, this class exists to provide a minimal set of features that don't need a database
11
+ # connection, and that will raise if a connection is attempted.
12
+ class UntenantedConnectionPool < ActiveRecord::ConnectionAdapters::NullPool # :nodoc:
13
+ attr_reader :db_config
14
+
15
+ def initialize(db_config, model)
16
+ super()
17
+
18
+ @db_config = db_config
19
+ @model = model
20
+ end
21
+
22
+ def schema_reflection
23
+ schema_cache_path = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(db_config)
24
+ ActiveRecord::ConnectionAdapters::SchemaReflection.new(schema_cache_path)
25
+ end
26
+
27
+ def schema_cache
28
+ ActiveRecord::ConnectionAdapters::BoundSchemaReflection.new(schema_reflection, self)
29
+ end
30
+
31
+ def lease_connection(...)
32
+ raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
33
+ end
34
+
35
+ def checkout(...)
36
+ raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
37
+ end
38
+
39
+ def with_connection(...)
40
+ raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
41
+ end
42
+
43
+ def new_connection(...)
44
+ raise Tenanted::NoTenantError, "Cannot connect to a tenanted database while untenanted (#{@model})."
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Activerecord
3
+ module ActiveRecord
4
4
  module Tenanted
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ require "zeitwerk"
6
+ loader = Zeitwerk::Loader.for_gem_extension(ActiveRecord)
7
+ loader.setup
8
+
9
+ module ActiveRecord
10
+ module Tenanted
11
+ # Base exception class for the library.
12
+ class Error < StandardError; end
13
+
14
+ # Raised when database access is attempted without a current tenant having been set.
15
+ class NoTenantError < Error; end
16
+
17
+ # Raised when database access is attempted on a record whose tenant does not match the current tenant.
18
+ class WrongTenantError < Error; end
19
+
20
+ # Raised when attempting to locate a GlobalID without a tenant.
21
+ class MissingTenantError < Error; end
22
+
23
+ # Raised when attempting to create a tenant that already exists.
24
+ class TenantExistsError < Error; end
25
+
26
+ # Raised when attempting to create a tenant with illegal characters in it.
27
+ class BadTenantNameError < Error; end
28
+
29
+ # Raised when the application's tenant configuration is invalid.
30
+ class TenantConfigurationError < Error; end
31
+
32
+ # Raised when implicit creation is disabled and a tenant is referenced that does not exist
33
+ class TenantDoesNotExistError < Error; end
34
+
35
+ # Raised when the Rails integration is being invoked but has not been configured.
36
+ class IntegrationNotConfiguredError < Error; end
37
+
38
+ def self.connection_class
39
+ # TODO: cache this / speed this up
40
+ Rails.application.config.active_record_tenanted.connection_class&.constantize
41
+ end
42
+ end
43
+ end
44
+
45
+ loader.eager_load
46
+
47
+ ActiveSupport.run_load_hooks :active_record_tenanted, ActiveRecord::Tenanted
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_record/tenanted"
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
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
57
+ end
58
+
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
68
+ 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
+ task "db:fixtures:load" => "db:tenant"
78
+ task "db:seed" => "db:tenant"