Skip to content

Commit 3143239

Browse files
authored
Name rake tasks with the database name (#223)
Blocking #222 This PR is mostly a refactoring, but brings some breaking changes that I think are worth the improvements in usability and extensibility. Specifically, the problem I'm facing right now is updating the rake tasks to be specific to a database, so for example `db:migrate:tenant` should be named `db:migrate:primary` (or more generally, `db:migrate:<DBNAME>`) so that if I have a secondary database it will have its own rake tasks like `db:migrate:secondary`. So the breaking change is around task names; however everything is still a dependency of the Rails tasks `db:migrate`, `db:prepare`, `db:drop`, and `db:reset` so it should be a huge deal for anybody (I hope). - db:migrate:DBNAME replaces db:migrate:tenant and db:migrate:tenant:all - it operates on all tenants by default - if there are no tenants it will create a database for the default tenant - the ARTENANT env var can be specified to run against a specific tenant - db:drop:DBNAME replaces db:drop:tenant - it operates on all tenants by default - NEW: the ARTENANT env var can be specified to run against a specific tenant - db:reset:DBNAME replaces db:reset:tenant - it operates on all tenants by default - NEW: the ARTENANT env var can be specified to run against a specific tenant - Tenanted::DatabaseTasks.base_config has been removed Some additional changes: - Tenanted::DatabaseTasks is now a class that takes a tenanted base config as a constructor parameter. Yay for encapsulation. - ActiveRecord::Tenanted.base_configs is a new method that returns all the tenanted base configs for the current environment.
2 parents f412b25 + 04fbce5 commit 3143239

File tree

9 files changed

+199
-186
lines changed

9 files changed

+199
-186
lines changed

GUIDE.md

Lines changed: 9 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -466,75 +466,17 @@ TODO:
466466
- [x] install a load hook
467467

468468
- database tasks
469-
- [x] make `db:migrate:tenant:all` iterate over all the tenants on disk
470-
- [x] make `db:migrate:tenant ARTENANT=asdf` run migrations on just that tenant
471-
- [x] make `db:migrate:tenant` run migrations on `development-tenant` in dev
472-
- [x] make `db:migrate` run `db:migrate:tenant` in dev
473-
- [x] make `db:prepare` run `db:migrate:tenant` in dev
469+
- [x] make `db:migrate:__dbname__` migrate all the existing tenants
470+
- [x] make `db:migrate:__dbname__ ARTENANT=asdf` run migrations on just that tenant
471+
- [x] make `db:drop:__dbname__` drop all the existing tenants
472+
- [x] make `db:drop:__dbname__ ARTENANT=asdf` drop just that tenant
473+
- [x] make `db:migrate` run `db:migrate:__dbname__`
474+
- [x] make `db:prepare` run `db:migrate:__dbname__`
475+
- [x] make `db:drop` run `db:drop:__dbname__`
474476
- [x] make a decision on what output tasks should emit, and whether we need a separate verbose setting
477+
- [x] use the database name instead of "tenant", e.g. "db:migrate:primary"
475478
- [ ] make the implicit migration opt-in
476-
- [ ] use the database name instead of "tenant", e.g. "db:migrate:primary"
477-
- [ ] fully implement all the relevant database tasks:
478-
- [ ] `db:_dump`
479-
- [ ] `db:_dump:__name__`
480-
- [ ] `db:abort_if_pending_migrations`
481-
- [ ] `db:abort_if_pending_migrations:__name__`
482-
- [ ] `db:charset`
483-
- [ ] `db:check_protected_environments`
484-
- [ ] `db:collation`
485-
- [ ] `db:create`
486-
- [ ] `db:create:all`
487-
- [ ] `db:create:__name__`
488-
- [ ] `db:drop`
489-
- [ ] `db:drop:_unsafe`
490-
- [ ] `db:drop:all`
491-
- [ ] `db:drop:__name__`
492-
- [ ] `db:encryption:init`
493-
- [ ] `db:environment:set`
494-
- [ ] `db:fixtures:identify`
495-
- [ ] `db:fixtures:load`
496-
- [ ] `db:forward`
497-
- [ ] `db:install:migrations`
498-
- [ ] `db:load_config`
499-
- [ ] `db:migrate` with support for VERSION
500-
- [ ] `db:migrate:down` with support for VERSION
501-
- [ ] `db:migrate:down:__name__`
502-
- [ ] `db:migrate:__name__`
503-
- [ ] `db:migrate:redo` with support for STEP and VERSION
504-
- [ ] `db:migrate:redo:__name__`
505-
- [ ] `db:migrate:reset`
506-
- [ ] `db:migrate:status`
507-
- [ ] `db:migrate:status:__name__`
508-
- [ ] `db:migrate:up` with support for VERSION
509-
- [ ] `db:migrate:up:__name__`
510-
- [ ] `db:prepare`
511-
- [ ] `db:purge` (see Known Issues below)
512-
- [ ] `db:purge:all` (see Known Issues below)
513-
- [ ] `db:reset`
514-
- [ ] `db:reset:all`
515-
- [ ] `db:reset:__name__`
516-
- [ ] `db:rollback` with support for STEP
517-
- [ ] `db:rollback:__name__`
518-
- [ ] `db:schema:cache:clear`
519-
- [ ] `db:schema:cache:dump`
520-
- [ ] `db:schema:dump`
521-
- [ ] `db:schema:dump:__name__`
522-
- [ ] `db:schema:load`
523-
- [ ] `db:schema:load:__name__`
524-
- [ ] `db:seed`
525-
- [ ] `db:seed:replant`
526-
- [ ] `db:setup`
527-
- [ ] `db:setup:all`
528-
- [ ] `db:setup:__name__`
529-
- [ ] `db:test:load_schema`
530-
- [ ] `db:test:load_schema:__name__`
531-
- [ ] `db:test:prepare`
532-
- [ ] `db:test:prepare:__name__`
533-
- [ ] `db:test:purge`
534-
- [ ] `db:test:purge:__name__`
535-
- [ ] `db:truncate_all`
536-
- [ ] `db:version`
537-
- [ ] `db:version:__name__`
479+
- [ ] fully implement all the relevant database tasks - see https://github.com/basecamp/activerecord-tenanted/issues/222
538480

539481
- installation
540482
- [ ] install a variation on the default database.yml with primary tenanted and non-primary "global" untenanted

lib/active_record/tenanted.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,19 @@ class IntegrationNotConfiguredError < Error; end
4141
# Raised when an unsupported database adapter is used.
4242
class UnsupportedDatabaseError < Error; end
4343

44+
# Return the constantized connection class configured in `config.active_record_tenanted.connection_class`,
45+
# or nil if none is configured.
4446
def self.connection_class
4547
# TODO: cache this / speed this up
4648
Rails.application.config.active_record_tenanted.connection_class&.constantize
4749
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
4857
end
4958
end
5059

lib/active_record/tenanted/console.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ module Tenanted
55
module Console # :nodoc:
66
module IRBConsole
77
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
913
super
1014
end
1115
end

lib/active_record/tenanted/database_tasks.rb

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,61 @@
11
# frozen_string_literal: true
22

3+
require "rake"
4+
35
module ActiveRecord
46
module Tenanted
5-
module DatabaseTasks # :nodoc:
6-
extend self
7-
8-
def migrate_all
9-
raise ArgumentError, "Could not find a tenanted database" unless config = base_config
7+
class DatabaseTasks # :nodoc:
8+
include Rake::DSL
109

11-
tenants = config.tenants.presence || [ get_current_tenant ].compact
12-
tenants.each do |tenant|
13-
tenant_config = config.new_tenant_config(tenant)
14-
migrate(tenant_config)
10+
class << self
11+
def verbose?
12+
ActiveRecord::Tasks::DatabaseTasks.send(:verbose?)
1513
end
1614
end
1715

18-
def migrate_tenant(tenant_name = set_current_tenant)
19-
raise ArgumentError, "Could not find a tenanted database" unless config = base_config
16+
attr_reader :config
2017

21-
tenant_config = config.new_tenant_config(tenant_name)
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
2224

23-
migrate(tenant_config)
25+
def migrate_all
26+
tenants = config.tenants.presence || [ get_default_tenant ].compact
27+
tenants.each do |tenant|
28+
migrate_tenant(tenant)
29+
end
2430
end
2531

26-
def drop_all
27-
raise ArgumentError, "Could not find a tenanted database" unless config = base_config
32+
def migrate_tenant(tenant = set_current_tenant)
33+
db_config = config.new_tenant_config(tenant)
34+
migrate(db_config)
35+
$stdout.puts "Migrated database '#{db_config.database}'" if verbose?
36+
end
2837

38+
def drop_all
2939
config.tenants.each do |tenant|
30-
db_config = config.new_tenant_config(tenant)
31-
db_config.config_adapter.drop_database
32-
$stdout.puts "Dropped database '#{db_config.database}'" if verbose?
40+
drop_tenant(tenant)
3341
end
3442
end
3543

36-
def base_config
37-
db_configs = ActiveRecord::Base.configurations.configs_for(
38-
env_name: ActiveRecord::Tasks::DatabaseTasks.env,
39-
include_hidden: true
40-
)
41-
db_configs.detect { |c| c.configuration_hash[:tenanted] }
44+
def drop_tenant(tenant = set_current_tenant)
45+
db_config = config.new_tenant_config(tenant)
46+
db_config.config_adapter.drop_database
47+
$stdout.puts "Dropped database '#{db_config.database}'" if verbose?
4248
end
4349

44-
def get_current_tenant
50+
def get_default_tenant
51+
# TODO: needs to work with multiple tenanted configs, maybe using ENV["ARTENANT_#{config.name}"]
4552
tenant = ENV["ARTENANT"]
4653

4754
if tenant.present?
4855
$stdout.puts "Setting current tenant to #{tenant.inspect}" if verbose?
4956
elsif Rails.env.local?
5057
tenant = Rails.application.config.active_record_tenanted.default_tenant
51-
$stdout.puts "Defaulting current tenant to #{tenant.inspect}" if verbose?
58+
$stdout.puts "Defaulting current tenant for #{config.name.inspect} to #{tenant.inspect}" if verbose?
5259
else
5360
tenant = nil
5461
$stdout.puts "Cannot determine an implicit tenant: ARTENANT not set, and Rails.env is not local." if verbose?
@@ -64,7 +71,7 @@ def set_current_tenant
6471
end
6572

6673
if connection_class.current_tenant.nil?
67-
connection_class.current_tenant = get_current_tenant
74+
connection_class.current_tenant = get_default_tenant
6875
else
6976
connection_class.current_tenant
7077
end
@@ -107,7 +114,49 @@ def migrate(config)
107114
end
108115

109116
def verbose?
110-
ActiveRecord::Tasks::DatabaseTasks.send(:verbose?)
117+
self.class.verbose?
118+
end
119+
120+
def register_rake_tasks
121+
name = config.name
122+
123+
desc "Migrate tenanted #{name} databases for current environment"
124+
task "db:migrate:#{name}" => "load_config" do
125+
verbose_was = ActiveRecord::Migration.verbose
126+
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
127+
128+
tenant = ENV["ARTENANT"]
129+
if tenant.present?
130+
migrate_tenant(tenant)
131+
else
132+
migrate_all
133+
end
134+
ensure
135+
ActiveRecord::Migration.verbose = verbose_was
136+
end
137+
task "db:migrate" => "db:migrate:#{name}"
138+
task "db:prepare" => "db:migrate:#{name}"
139+
140+
desc "Drop tenanted #{name} databases for current environment"
141+
task "db:drop:#{name}" => "load_config" do
142+
verbose_was = ActiveRecord::Migration.verbose
143+
ActiveRecord::Migration.verbose = ActiveRecord::Tenanted::DatabaseTasks.verbose?
144+
145+
tenant = ENV["ARTENANT"]
146+
if tenant.present?
147+
drop_tenant(tenant)
148+
else
149+
drop_all
150+
end
151+
ensure
152+
ActiveRecord::Migration.verbose = verbose_was
153+
end
154+
task "db:drop" => "db:drop:#{name}"
155+
156+
# TODO: Rails' database tasks include "db:seed" in the tasks that "db:reset" runs.
157+
desc "Drop and recreate tenanted #{name} database from its schema for the current environment"
158+
task "db:reset:#{name}" => [ "db:drop:#{name}", "db:migrate:#{name}" ]
159+
task "db:reset" => "db:reset:#{name}"
111160
end
112161
end
113162
end

lib/active_record/tenanted/tenant.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,16 @@ def with_tenant(tenant_name, prohibit_shard_swapping: true, &block)
120120

121121
def create_tenant(tenant_name, if_not_exists: false, &block)
122122
created_db = false
123-
adapter = tenanted_root_config.new_tenant_config(tenant_name).config_adapter
123+
base_config = tenanted_root_config
124+
adapter = base_config.new_tenant_config(tenant_name).config_adapter
124125

125126
adapter.acquire_ready_lock do
126127
unless adapter.database_exist?
127128
adapter.create_database
128129

129130
with_tenant(tenant_name) do
130131
connection_pool(schema_version_check: false)
131-
ActiveRecord::Tenanted::DatabaseTasks.migrate_tenant(tenant_name)
132+
ActiveRecord::Tenanted::DatabaseTasks.new(base_config).migrate_tenant(tenant_name)
132133
end
133134

134135
created_db = true
Lines changed: 13 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,20 @@
11
# frozen_string_literal: true
22

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.base_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.base_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
579
end
5810

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
6813
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.
7714
task "db:fixtures:load" => "db:tenant"
7815
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

test/test_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ def test(name, &block)
5151
super
5252
end
5353

54+
def for_each_db_scenario(s = all_scenarios, &block)
55+
s.each_key do |db_scenario|
56+
with_db_scenario(db_scenario, &block)
57+
end
58+
end
59+
5460
def for_each_scenario(s = all_scenarios, except: {}, &block)
5561
s.each do |db_scenario, model_scenarios|
5662
with_db_scenario(db_scenario) do

0 commit comments

Comments
 (0)