activerecord-tenanted 0.1.0 → 0.3.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 +205 -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 +261 -0
  21. data/lib/active_record/tenanted/tenant_selector.rb +54 -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 +79 -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,261 @@
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 inspect
20
+ tenant ? super.sub(/>$/, ", tenant=#{tenant.inspect}>") : super
21
+ end
22
+
23
+ def to_global_id(options = {})
24
+ super(options.merge(tenant: tenant))
25
+ end
26
+
27
+ def to_signed_global_id(options = {})
28
+ super(options.merge(tenant: tenant))
29
+ end
30
+
31
+ def association(name)
32
+ super.tap do |assoc|
33
+ if assoc.reflection.polymorphic? || assoc.reflection.klass.tenanted?
34
+ ensure_tenant_context_safety
35
+ end
36
+ end
37
+ end
38
+
39
+ alias to_gid to_global_id
40
+ alias to_sgid to_signed_global_id
41
+
42
+ private
43
+ # I would prefer to do this in an `after_initialize` callback, but some associations are
44
+ # created before those callbacks are invoked (for example, a `belongs_to` association) and
45
+ # we need to be able to ensure tenant context safety on all associations.
46
+ def init_internals
47
+ @tenant = self.class.current_tenant
48
+ super
49
+ end
50
+
51
+ def ensure_tenant_context_safety
52
+ self_tenant = self.tenant
53
+ current_tenant = self.class.current_tenant
54
+
55
+ if current_tenant.nil?
56
+ raise NoTenantError, "Cannot connect to a tenanted database while untenanted (#{self.class})"
57
+ elsif self_tenant != current_tenant
58
+ raise WrongTenantError,
59
+ "#{self.class} model belongs to tenant #{self_tenant.inspect}, " \
60
+ "but current tenant is #{current_tenant.inspect}"
61
+ end
62
+ end
63
+ end
64
+
65
+ module Tenant
66
+ extend ActiveSupport::Concern
67
+
68
+ # This is a sentinel value used to indicate that the class is not currently tenanted.
69
+ #
70
+ # It's the default value returned by `current_shard` when the class is not tenanted. The
71
+ # `current_tenant` method's job is to recognizes that sentinel value and return `nil`, because
72
+ # Active Record itself does not recognize `nil` as a valid shard value.
73
+ UNTENANTED_SENTINEL = Class.new do # :nodoc:
74
+ def inspect
75
+ "ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL"
76
+ end
77
+
78
+ def to_s
79
+ "(untenanted)"
80
+ end
81
+ end.new.freeze
82
+
83
+ CONNECTION_POOL_CREATION_LOCK = Thread::Mutex.new # :nodoc:
84
+
85
+ class_methods do
86
+ def tenanted?
87
+ true
88
+ end
89
+
90
+ def current_tenant
91
+ shard = current_shard
92
+ shard != UNTENANTED_SENTINEL ? shard : nil
93
+ end
94
+
95
+ def current_tenant=(tenant_name)
96
+ tenant_name = tenant_name.to_s unless tenant_name == UNTENANTED_SENTINEL
97
+
98
+ connection_class_for_self.connecting_to(shard: tenant_name, role: ActiveRecord.writing_role)
99
+ end
100
+
101
+ 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)
106
+ end
107
+
108
+ def with_tenant(tenant_name, prohibit_shard_swapping: true, &block)
109
+ tenant_name = tenant_name.to_s unless tenant_name == UNTENANTED_SENTINEL
110
+
111
+ if tenant_name == current_tenant
112
+ yield
113
+ else
114
+ connection_class_for_self.connected_to(shard: tenant_name, role: ActiveRecord.writing_role) do
115
+ prohibit_shard_swapping(prohibit_shard_swapping) do
116
+ log_tenant_tag(tenant_name, &block)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def create_tenant(tenant_name, if_not_exists: false, &block)
123
+ created_db = false
124
+ database_path = tenanted_root_config.database_path_for(tenant_name)
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)
132
+
133
+ with_tenant(tenant_name) do
134
+ connection_pool(schema_version_check: false)
135
+ ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant(tenant_name)
136
+ end
137
+
138
+ created_db = true
139
+ end
140
+ rescue
141
+ FileUtils.rm_f(database_path)
142
+ raise
143
+ end
144
+
145
+ raise TenantExistsError unless created_db || if_not_exists
146
+
147
+ with_tenant(tenant_name) do
148
+ yield if block_given?
149
+ end
150
+ end
151
+
152
+ def destroy_tenant(tenant_name)
153
+ ActiveRecord::Base.logger.info " DESTROY [tenant=#{tenant_name}] Destroying tenant database"
154
+
155
+ with_tenant(tenant_name, prohibit_shard_swapping: false) do
156
+ if retrieve_connection_pool(strict: false)
157
+ remove_connection
158
+ end
159
+ end
160
+
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))
165
+ end
166
+
167
+ def tenants
168
+ # DatabaseConfigurations::RootConfig#tenants returns all tenants whose database files
169
+ # exist, but some of those may be getting initially migrated, so we perform an additional
170
+ # filter on readiness with `tenant_exist?`.
171
+ tenanted_root_config.tenants.select { |t| tenant_exist?(t) }
172
+ end
173
+
174
+ def with_each_tenant(**options, &block)
175
+ tenants.each { |tenant| with_tenant(tenant, **options) { yield tenant } }
176
+ end
177
+
178
+ # This method is really only intended to be used for testing.
179
+ def without_tenant(&block) # :nodoc:
180
+ with_tenant(ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL, prohibit_shard_swapping: false, &block)
181
+ end
182
+
183
+ def connection_pool(schema_version_check: true) # :nodoc:
184
+ if current_tenant
185
+ pool = retrieve_connection_pool(strict: false)
186
+
187
+ if pool.nil?
188
+ CONNECTION_POOL_CREATION_LOCK.synchronize do
189
+ # re-check now that we have the lock
190
+ pool = retrieve_connection_pool(strict: false)
191
+
192
+ if pool.nil?
193
+ _create_tenanted_pool(schema_version_check: schema_version_check)
194
+ pool = retrieve_connection_pool(strict: true)
195
+ end
196
+ end
197
+ end
198
+
199
+ pool
200
+ else
201
+ Tenanted::UntenantedConnectionPool.new(tenanted_root_config, self)
202
+ end
203
+ end
204
+
205
+ def tenanted_root_config # :nodoc:
206
+ ActiveRecord::Base.configurations.resolve(tenanted_config_name.to_sym)
207
+ end
208
+
209
+ def tenanted_config_name # :nodoc:
210
+ @tenanted_config_name ||= (superclass.respond_to?(:tenanted_config_name) ? superclass.tenanted_config_name : nil)
211
+ end
212
+
213
+ def _create_tenanted_pool(schema_version_check: true) # :nodoc:
214
+ # ensure all classes use the same connection pool
215
+ return superclass._create_tenanted_pool unless connection_class?
216
+
217
+ tenant = current_tenant
218
+ unless File.exist?(tenanted_root_config.database_path_for(tenant))
219
+ raise TenantDoesNotExistError, "The database file for tenant #{tenant.inspect} does not exist."
220
+ end
221
+
222
+ config = tenanted_root_config.new_tenant_config(tenant)
223
+ pool = establish_connection(config)
224
+
225
+ if schema_version_check
226
+ pending_migrations = pool.migration_context.open.pending_migrations
227
+ raise ActiveRecord::PendingMigrationError.new(pending_migrations: pending_migrations) if pending_migrations.any?
228
+ end
229
+
230
+ pool
231
+ end
232
+
233
+ private
234
+ def retrieve_connection_pool(strict:)
235
+ connection_handler.retrieve_connection_pool(connection_specification_name,
236
+ role: current_role,
237
+ shard: current_tenant,
238
+ strict: strict)
239
+ end
240
+
241
+ def log_tenant_tag(tenant_name, &block)
242
+ if Rails.application.config.active_record_tenanted.log_tenant_tag
243
+ Rails.logger.tagged("tenant=#{tenant_name}", &block)
244
+ else
245
+ yield
246
+ end
247
+ end
248
+ end
249
+
250
+ prepended do
251
+ self.default_shard = ActiveRecord::Tenanted::Tenant::UNTENANTED_SENTINEL
252
+
253
+ prepend TenantCommon
254
+ end
255
+
256
+ def tenanted?
257
+ true
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Tenanted
5
+ #
6
+ # If config.active_record_tenanted.connection_class is set, this middleware will be loaded
7
+ # automatically, and will use config.active_record_tenanted.tenant_resolver to determine the
8
+ # appropriate tenant for the request.
9
+ #
10
+ # If no tenant is resolved, the request will be executed without wrapping it in a tenanted
11
+ # context. Application code will be free to set the tenant as needed.
12
+ #
13
+ # If a tenant is resolved and the tenant exists, the application will be locked to that
14
+ # tenant's database for the duration of the request.
15
+ #
16
+ # If a tenant is resolved, but the tenant does not exist, a 404 response will be returned.
17
+ #
18
+ class TenantSelector
19
+ attr_reader :app
20
+
21
+ def initialize(app)
22
+ @app = app
23
+ end
24
+
25
+ def call(env)
26
+ request = ActionDispatch::Request.new(env)
27
+ tenant_name = tenant_resolver.call(request)
28
+
29
+ if tenant_name.blank?
30
+ # run the request without wrapping it in a tenanted context
31
+ @app.call(env)
32
+ elsif tenanted_class.tenant_exist?(tenant_name)
33
+ tenanted_class.with_tenant(tenant_name) { @app.call(env) }
34
+ else
35
+ raise ActiveRecord::Tenanted::TenantDoesNotExistError, "Tenant not found: #{tenant_name.inspect}"
36
+ end
37
+ end
38
+
39
+ def tenanted_class
40
+ # Note: we'll probably want to cache this when we look at performance, but don't cache it
41
+ # when class reloading is enabled.
42
+ tenanted_class_name.constantize
43
+ end
44
+
45
+ def tenanted_class_name
46
+ @tenanted_class_name ||= Rails.application.config.active_record_tenanted.connection_class
47
+ end
48
+
49
+ def tenant_resolver
50
+ @tenanted_resolver ||= Rails.application.config.active_record_tenanted.tenant_resolver
51
+ end
52
+ end
53
+ end
54
+ 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.3.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"