ros-apartment 3.2.0 → 3.4.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +99 -2
  4. data/.ruby-version +1 -1
  5. data/AGENTS.md +19 -0
  6. data/Appraisals +15 -0
  7. data/CLAUDE.md +210 -0
  8. data/README.md +55 -8
  9. data/Rakefile +6 -5
  10. data/docs/adapters.md +177 -0
  11. data/docs/architecture.md +274 -0
  12. data/docs/elevators.md +226 -0
  13. data/docs/images/log_example.png +0 -0
  14. data/lib/apartment/CLAUDE.md +300 -0
  15. data/lib/apartment/adapters/CLAUDE.md +314 -0
  16. data/lib/apartment/adapters/abstract_adapter.rb +24 -15
  17. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
  18. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
  19. data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
  20. data/lib/apartment/adapters/postgresql_adapter.rb +42 -19
  21. data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
  22. data/lib/apartment/console.rb +1 -1
  23. data/lib/apartment/custom_console.rb +7 -7
  24. data/lib/apartment/elevators/CLAUDE.md +292 -0
  25. data/lib/apartment/elevators/domain.rb +1 -1
  26. data/lib/apartment/elevators/generic.rb +1 -1
  27. data/lib/apartment/elevators/host_hash.rb +3 -3
  28. data/lib/apartment/elevators/subdomain.rb +9 -5
  29. data/lib/apartment/log_subscriber.rb +1 -1
  30. data/lib/apartment/migrator.rb +2 -2
  31. data/lib/apartment/model.rb +1 -1
  32. data/lib/apartment/railtie.rb +3 -3
  33. data/lib/apartment/tasks/CLAUDE.md +107 -0
  34. data/lib/apartment/tasks/enhancements.rb +1 -1
  35. data/lib/apartment/tasks/schema_dumper.rb +109 -0
  36. data/lib/apartment/tasks/task_helper.rb +273 -35
  37. data/lib/apartment/tenant.rb +3 -3
  38. data/lib/apartment/version.rb +1 -1
  39. data/lib/apartment.rb +35 -10
  40. data/lib/generators/apartment/install/install_generator.rb +1 -1
  41. data/lib/generators/apartment/install/templates/apartment.rb +2 -2
  42. data/lib/tasks/apartment.rake +64 -37
  43. data/ros-apartment.gemspec +3 -3
  44. metadata +26 -15
@@ -1,54 +1,292 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/module/delegation'
4
+
3
5
  module Apartment
6
+ # Coordinates tenant operations for rake tasks with parallel execution support.
7
+ #
8
+ # ## Problem Context
9
+ #
10
+ # Multi-tenant applications with many schemas face slow migration times when
11
+ # running sequentially. A 100-tenant system with 2-second migrations takes
12
+ # 3+ minutes sequentially but ~20 seconds with 10 parallel workers.
13
+ #
14
+ # ## Why This Design
15
+ #
16
+ # Parallel database migrations introduce two categories of problems:
17
+ #
18
+ # 1. **Platform-specific fork safety**: macOS/Windows have issues with libpq
19
+ # (PostgreSQL C library) after fork() due to GSS/Kerberos state corruption.
20
+ # Linux handles fork() cleanly. We auto-detect and choose the safe strategy.
21
+ #
22
+ # 2. **PostgreSQL advisory lock deadlocks**: Rails uses advisory locks to
23
+ # prevent concurrent migrations. When multiple processes/threads migrate
24
+ # different schemas simultaneously, they deadlock competing for the same
25
+ # lock. We disable advisory locks during parallel execution, which means
26
+ # **you accept responsibility for ensuring your migrations are parallel-safe**.
27
+ #
28
+ # ## When to Use Parallel Migrations
29
+ #
30
+ # This is an advanced feature. Use it when:
31
+ # - You have many tenants and sequential migration time is problematic
32
+ # - Your migrations only modify tenant-specific schema objects
33
+ # - You've verified your migrations don't have cross-schema side effects
34
+ #
35
+ # Stick with sequential execution (the default) when:
36
+ # - Migrations create/modify extensions, types, or shared objects
37
+ # - Migrations have ordering dependencies across tenants
38
+ # - You're unsure whether parallel execution is safe for your use case
39
+ #
40
+ # ## Gotchas
41
+ #
42
+ # - The `parallel_migration_threads` count should be less than your connection
43
+ # pool size to avoid connection exhaustion.
44
+ # - Empty/nil tenant names from `tenant_names` proc are filtered to prevent
45
+ # PostgreSQL "zero-length delimited identifier" errors.
46
+ # - Process-based parallelism requires fresh connections in each fork;
47
+ # thread-based parallelism shares the pool but needs explicit checkout.
48
+ #
49
+ # @see Apartment.parallel_migration_threads
50
+ # @see Apartment.parallel_strategy
51
+ # @see Apartment.manage_advisory_locks
4
52
  module TaskHelper
5
- def self.each_tenant(&block)
6
- Parallel.each(tenants_without_default, in_threads: Apartment.parallel_migration_threads) do |tenant|
7
- Rails.application.executor.wrap do
8
- block.call(tenant)
53
+ # Captures outcome per tenant for aggregated reporting. Allows migrations
54
+ # to continue for remaining tenants even when one fails.
55
+ Result = Struct.new(:tenant, :success, :error, keyword_init: true)
56
+
57
+ class << self
58
+ # Primary entry point for tenant iteration. Automatically selects
59
+ # sequential or parallel execution based on configuration.
60
+ #
61
+ # @yield [String] tenant name
62
+ # @return [Array<Result>] outcome for each tenant
63
+ def each_tenant(&)
64
+ return [] if tenants_without_default.empty?
65
+
66
+ if parallel_migration_threads.positive?
67
+ each_tenant_parallel(&)
68
+ else
69
+ each_tenant_sequential(&)
9
70
  end
10
71
  end
11
- end
12
72
 
13
- def self.tenants_without_default
14
- tenants - [Apartment.default_tenant]
15
- end
73
+ # Sequential execution: simpler, no connection management complexity.
74
+ # Used when parallel_migration_threads is 0 (the default).
75
+ def each_tenant_sequential
76
+ tenants_without_default.map do |tenant|
77
+ Rails.application.executor.wrap do
78
+ yield(tenant)
79
+ end
80
+ Result.new(tenant: tenant, success: true, error: nil)
81
+ rescue StandardError => e
82
+ Result.new(tenant: tenant, success: false, error: e.message)
83
+ end
84
+ end
16
85
 
17
- def self.tenants
18
- ENV['DB'] ? ENV['DB'].split(',').map(&:strip) : Apartment.tenant_names || []
19
- end
86
+ # Parallel execution wrapper. Disables advisory locks for the duration,
87
+ # then delegates to platform-appropriate parallelism strategy.
88
+ def each_tenant_parallel(&)
89
+ with_advisory_locks_disabled do
90
+ case resolve_parallel_strategy
91
+ when :processes
92
+ each_tenant_in_processes(&)
93
+ else
94
+ each_tenant_in_threads(&)
95
+ end
96
+ end
97
+ end
20
98
 
21
- def self.warn_if_tenants_empty
22
- return unless tenants.empty? && ENV['IGNORE_EMPTY_TENANTS'] != 'true'
99
+ # Process-based parallelism via fork(). Faster on Linux due to
100
+ # copy-on-write memory and no GIL contention. Each forked process
101
+ # gets isolated memory, so we must clear inherited connections
102
+ # and establish fresh ones.
103
+ def each_tenant_in_processes
104
+ Parallel.map(tenants_without_default, in_processes: parallel_migration_threads) do |tenant|
105
+ # Forked processes inherit parent's connection handles but the
106
+ # underlying sockets are invalid. Must reconnect before any DB work.
107
+ ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
108
+ reconnect_for_parallel_execution
23
109
 
24
- puts <<-WARNING
25
- [WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things:
110
+ Rails.application.executor.wrap do
111
+ yield(tenant)
112
+ end
113
+ Result.new(tenant: tenant, success: true, error: nil)
114
+ rescue StandardError => e
115
+ Result.new(tenant: tenant, success: false, error: e.message)
116
+ ensure
117
+ ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
118
+ end
119
+ end
26
120
 
27
- 1. You may not have created any, in which case you can ignore this message
28
- 2. You've run `apartment:migrate` directly without loading the Rails environment
29
- * `apartment:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate`
121
+ # Thread-based parallelism. Safe on all platforms but subject to GIL
122
+ # for CPU-bound work (migrations are typically I/O-bound, so this is fine).
123
+ # Threads share the connection pool, so we reconfigure once before
124
+ # spawning and restore after completion.
125
+ def each_tenant_in_threads
126
+ original_config = ActiveRecord::Base.connection_db_config.configuration_hash
127
+ reconnect_for_parallel_execution
30
128
 
31
- Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this.
32
- WARNING
33
- end
129
+ Parallel.map(tenants_without_default, in_threads: parallel_migration_threads) do |tenant|
130
+ # Explicit connection checkout prevents pool exhaustion when
131
+ # thread count exceeds pool size minus buffer.
132
+ ActiveRecord::Base.connection_pool.with_connection do
133
+ Rails.application.executor.wrap do
134
+ yield(tenant)
135
+ end
136
+ end
137
+ Result.new(tenant: tenant, success: true, error: nil)
138
+ rescue StandardError => e
139
+ Result.new(tenant: tenant, success: false, error: e.message)
140
+ end
141
+ ensure
142
+ ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
143
+ ActiveRecord::Base.establish_connection(original_config) if original_config
144
+ end
34
145
 
35
- def self.create_tenant(tenant_name)
36
- puts("Creating #{tenant_name} tenant")
37
- Apartment::Tenant.create(tenant_name)
38
- rescue Apartment::TenantExists => e
39
- puts "Tried to create already existing tenant: #{e}"
40
- end
146
+ # Auto-detection logic for parallelism strategy. Only Linux gets
147
+ # process-based parallelism by default due to macOS libpq fork issues.
148
+ def resolve_parallel_strategy
149
+ strategy = Apartment.parallel_strategy
150
+
151
+ return :threads if strategy == :threads
152
+ return :processes if strategy == :processes
153
+
154
+ fork_safe_platform? ? :processes : :threads
155
+ end
156
+
157
+ # Platform detection. Conservative: only Linux is considered fork-safe.
158
+ # macOS has documented issues with libpq, GSS-API, and Kerberos after fork.
159
+ # See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
160
+ def fork_safe_platform?
161
+ RUBY_PLATFORM.include?('linux')
162
+ end
163
+
164
+ # Advisory lock management. Rails acquires pg_advisory_lock during migrations
165
+ # to prevent concurrent schema changes. With parallel tenant migrations,
166
+ # this causes deadlocks since all workers compete for the same lock.
167
+ #
168
+ # **Important**: Disabling advisory locks shifts responsibility to you.
169
+ # Your migrations must be safe to run concurrently across tenants. If your
170
+ # migrations modify shared resources, create extensions, or have other
171
+ # cross-schema side effects, parallel execution may cause failures.
172
+ # When in doubt, use sequential execution (parallel_migration_threads = 0).
173
+ #
174
+ # Uses ENV var because Rails checks it at connection establishment time,
175
+ # and we need it disabled before Parallel spawns workers.
176
+ def with_advisory_locks_disabled
177
+ return yield unless parallel_migration_threads.positive?
178
+ return yield unless Apartment.manage_advisory_locks
179
+
180
+ original_env_value = ENV.fetch('DISABLE_ADVISORY_LOCKS', nil)
181
+ begin
182
+ ENV['DISABLE_ADVISORY_LOCKS'] = 'true'
183
+ yield
184
+ ensure
185
+ if original_env_value.nil?
186
+ ENV.delete('DISABLE_ADVISORY_LOCKS')
187
+ else
188
+ ENV['DISABLE_ADVISORY_LOCKS'] = original_env_value
189
+ end
190
+ end
191
+ end
41
192
 
42
- def self.migrate_tenant(tenant_name)
43
- strategy = Apartment.db_migrate_tenant_missing_strategy
44
- create_tenant(tenant_name) if strategy == :create_tenant
193
+ # Re-establishes database connection for parallel execution.
194
+ # When manage_advisory_locks is true, disables advisory locks in the
195
+ # connection config (belt-and-suspenders with the ENV var approach).
196
+ # When false, reconnects with existing config unchanged.
197
+ def reconnect_for_parallel_execution
198
+ current_config = ActiveRecord::Base.connection_db_config.configuration_hash
45
199
 
46
- puts("Migrating #{tenant_name} tenant")
47
- Apartment::Migrator.migrate tenant_name
48
- rescue Apartment::TenantNotFound => e
49
- raise e if strategy == :raise_exception
200
+ new_config = if Apartment.manage_advisory_locks
201
+ current_config.merge(advisory_locks: false)
202
+ else
203
+ current_config
204
+ end
50
205
 
51
- puts e.message
206
+ ActiveRecord::Base.establish_connection(new_config)
207
+ end
208
+
209
+ # Delegate to Apartment.parallel_migration_threads
210
+ delegate :parallel_migration_threads, to: Apartment
211
+
212
+ # Get list of tenants excluding the default tenant
213
+ # Also filters out blank/empty tenant names to prevent errors
214
+ #
215
+ # @return [Array<String>] tenant names
216
+ def tenants_without_default
217
+ (tenants - [Apartment.default_tenant]).reject { |t| t.nil? || t.to_s.strip.empty? }
218
+ end
219
+
220
+ # Get list of all tenants to operate on
221
+ # Supports DB env var for targeting specific tenants
222
+ # Filters out blank tenant names for safety
223
+ #
224
+ # @return [Array<String>] tenant names
225
+ def tenants
226
+ result = ENV['DB'] ? ENV['DB'].split(',').map(&:strip) : Apartment.tenant_names || []
227
+ result.reject { |t| t.nil? || t.to_s.strip.empty? }
228
+ end
229
+
230
+ # Display warning if tenant list is empty
231
+ def warn_if_tenants_empty
232
+ return unless tenants.empty? && ENV['IGNORE_EMPTY_TENANTS'] != 'true'
233
+
234
+ puts <<~WARNING
235
+ [WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things:
236
+
237
+ 1. You may not have created any, in which case you can ignore this message
238
+ 2. You've run `apartment:migrate` directly without loading the Rails environment
239
+ * `apartment:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate`
240
+
241
+ Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this.
242
+ WARNING
243
+ end
244
+
245
+ # Display summary of operation results
246
+ #
247
+ # @param operation [String] name of the operation (e.g., "Migration", "Rollback")
248
+ # @param results [Array<Result>] results from each_tenant
249
+ def display_summary(operation, results)
250
+ return if results.empty?
251
+
252
+ succeeded = results.count(&:success)
253
+ failed = results.reject(&:success)
254
+
255
+ puts "\n=== #{operation} Summary ==="
256
+ puts "Succeeded: #{succeeded}/#{results.size} tenants"
257
+
258
+ return if failed.empty?
259
+
260
+ puts "Failed: #{failed.size} tenants"
261
+ failed.each do |result|
262
+ puts " - #{result.tenant}: #{result.error}"
263
+ end
264
+ end
265
+
266
+ # Create a tenant with logging
267
+ #
268
+ # @param tenant_name [String] name of tenant to create
269
+ def create_tenant(tenant_name)
270
+ puts("Creating #{tenant_name} tenant")
271
+ Apartment::Tenant.create(tenant_name)
272
+ rescue Apartment::TenantExists => e
273
+ puts "Tried to create already existing tenant: #{e}"
274
+ end
275
+
276
+ # Migrate a single tenant with error handling based on strategy
277
+ #
278
+ # @param tenant_name [String] name of tenant to migrate
279
+ def migrate_tenant(tenant_name)
280
+ strategy = Apartment.db_migrate_tenant_missing_strategy
281
+ create_tenant(tenant_name) if strategy == :create_tenant
282
+
283
+ puts("Migrating #{tenant_name} tenant")
284
+ Apartment::Migrator.migrate(tenant_name)
285
+ rescue Apartment::TenantNotFound => e
286
+ raise(e) if strategy == :raise_exception
287
+
288
+ puts e.message
289
+ end
52
290
  end
53
291
  end
54
292
  end
@@ -32,13 +32,13 @@ module Apartment
32
32
  end
33
33
 
34
34
  begin
35
- require "apartment/adapters/#{adapter_method}"
35
+ require("apartment/adapters/#{adapter_method}")
36
36
  rescue LoadError
37
- raise "The adapter `#{adapter_method}` is not yet supported"
37
+ raise("The adapter `#{adapter_method}` is not yet supported")
38
38
  end
39
39
 
40
40
  unless respond_to?(adapter_method)
41
- raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
41
+ raise(AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter")
42
42
  end
43
43
 
44
44
  send(adapter_method, config)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apartment
4
- VERSION = '3.2.0'
4
+ VERSION = '3.4.0'
5
5
  end
data/lib/apartment.rb CHANGED
@@ -28,7 +28,8 @@ module Apartment
28
28
  WRITER_METHODS = %i[tenant_names database_schema_file excluded_models
29
29
  persistent_schemas connection_class
30
30
  db_migrate_tenants db_migrate_tenant_missing_strategy seed_data_file
31
- parallel_migration_threads pg_excluded_names].freeze
31
+ parallel_migration_threads pg_excluded_names
32
+ parallel_strategy manage_advisory_locks].freeze
32
33
 
33
34
  attr_accessor(*ACCESSOR_METHODS)
34
35
  attr_writer(*WRITER_METHODS)
@@ -41,7 +42,7 @@ module Apartment
41
42
 
42
43
  # configure apartment with available options
43
44
  def configure
44
- yield self if block_given?
45
+ yield(self) if block_given?
45
46
  end
46
47
 
47
48
  def tenant_names
@@ -57,7 +58,7 @@ module Apartment
57
58
  end
58
59
 
59
60
  def db_config_for(tenant)
60
- (tenants_with_config[tenant] || connection_config)
61
+ tenants_with_config[tenant] || connection_config
61
62
  end
62
63
 
63
64
  # Whether or not db:migrate should also migrate tenants
@@ -68,9 +69,10 @@ module Apartment
68
69
  @db_migrate_tenants = true
69
70
  end
70
71
 
71
- # How to handle tenant missing on db:migrate
72
- # defaults to :rescue_exception
73
- # available options: rescue_exception, raise_exception, create_tenant
72
+ # How to handle missing tenants during db:migrate
73
+ # :rescue_exception (default) - Log error, continue with other tenants
74
+ # :raise_exception - Stop migration immediately
75
+ # :create_tenant - Automatically create missing tenant and migrate
74
76
  def db_migrate_tenant_missing_strategy
75
77
  valid = %i[rescue_exception raise_exception create_tenant]
76
78
  value = @db_migrate_tenant_missing_strategy || :rescue_exception
@@ -80,7 +82,7 @@ module Apartment
80
82
  key_name = 'config.db_migrate_tenant_missing_strategy'
81
83
  opt_names = valid.join(', ')
82
84
 
83
- raise ApartmentError, "Option #{value} not valid for `#{key_name}`. Use one of #{opt_names}"
85
+ raise(ApartmentError, "Option #{value} not valid for `#{key_name}`. Use one of #{opt_names}")
84
86
  end
85
87
 
86
88
  # Default to empty array
@@ -92,6 +94,24 @@ module Apartment
92
94
  @parallel_migration_threads || 0
93
95
  end
94
96
 
97
+ # Parallelism strategy for migrations
98
+ # :auto (default) - Detect platform: processes on Linux, threads on macOS/Windows
99
+ # :threads - Always use threads (safer, works everywhere)
100
+ # :processes - Always use processes (faster on Linux, may crash on macOS/Windows)
101
+ def parallel_strategy
102
+ @parallel_strategy || :auto
103
+ end
104
+
105
+ # Whether to manage PostgreSQL advisory locks during parallel migrations
106
+ # When true and parallel_migration_threads > 0, advisory locks are disabled
107
+ # during migration to prevent deadlocks, then restored afterward.
108
+ # Default: true
109
+ def manage_advisory_locks
110
+ return @manage_advisory_locks if defined?(@manage_advisory_locks)
111
+
112
+ @manage_advisory_locks = true
113
+ end
114
+
95
115
  def persistent_schemas
96
116
  @persistent_schemas || []
97
117
  end
@@ -126,14 +146,19 @@ module Apartment
126
146
  def extract_tenant_config
127
147
  return {} unless @tenant_names
128
148
 
149
+ # Execute callable (proc/lambda) to get dynamic tenant list from database
129
150
  values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
130
- unless values.is_a? Hash
131
- values = values.each_with_object({}) do |tenant, hash|
132
- hash[tenant] = connection_config
151
+
152
+ # Normalize arrays to hash format (tenant_name => connection_config)
153
+ unless values.is_a?(Hash)
154
+ values = values.index_with do |_tenant|
155
+ connection_config
133
156
  end
134
157
  end
135
158
  values.with_indifferent_access
136
159
  rescue ActiveRecord::StatementInvalid
160
+ # Database query failed (table doesn't exist yet, connection issue)
161
+ # Return empty hash to allow app to boot without tenants
137
162
  {}
138
163
  end
139
164
  end
@@ -5,7 +5,7 @@ module Apartment
5
5
  source_root File.expand_path('templates', __dir__)
6
6
 
7
7
  def copy_files
8
- template 'apartment.rb', File.join('config', 'initializers', 'apartment.rb')
8
+ template('apartment.rb', File.join('config', 'initializers', 'apartment.rb'))
9
9
  end
10
10
  end
11
11
  end
@@ -50,7 +50,7 @@ Apartment.configure do |config|
50
50
  # end
51
51
  # end
52
52
  #
53
- config.tenant_names = -> { ToDo_Tenant_Or_User_Model.pluck :database }
53
+ config.tenant_names = -> { ToDo_Tenant_Or_User_Model.pluck(:database) }
54
54
 
55
55
  # PostgreSQL:
56
56
  # Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
@@ -111,6 +111,6 @@ end
111
111
  # }
112
112
 
113
113
  # Rails.application.config.middleware.use Apartment::Elevators::Domain
114
- Rails.application.config.middleware.use Apartment::Elevators::Subdomain
114
+ Rails.application.config.middleware.use(Apartment::Elevators::Subdomain)
115
115
  # Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain
116
116
  # Rails.application.config.middleware.use Apartment::Elevators::Host
@@ -2,11 +2,12 @@
2
2
 
3
3
  require 'apartment/migrator'
4
4
  require 'apartment/tasks/task_helper'
5
+ require 'apartment/tasks/schema_dumper'
5
6
  require 'parallel'
6
7
 
7
- apartment_namespace = namespace :apartment do
8
- desc 'Create all tenants'
9
- task :create do
8
+ apartment_namespace = namespace(:apartment) do
9
+ desc('Create all tenants')
10
+ task(create: :environment) do
10
11
  Apartment::TaskHelper.warn_if_tenants_empty
11
12
 
12
13
  Apartment::TaskHelper.tenants.each do |tenant|
@@ -14,8 +15,8 @@ apartment_namespace = namespace :apartment do
14
15
  end
15
16
  end
16
17
 
17
- desc 'Drop all tenants'
18
- task :drop do
18
+ desc('Drop all tenants')
19
+ task(drop: :environment) do
19
20
  Apartment::TaskHelper.tenants.each do |tenant|
20
21
  puts("Dropping #{tenant} tenant")
21
22
  Apartment::Tenant.drop(tenant)
@@ -24,16 +25,29 @@ apartment_namespace = namespace :apartment do
24
25
  end
25
26
  end
26
27
 
27
- desc 'Migrate all tenants'
28
- task :migrate do
28
+ desc('Migrate all tenants')
29
+ task(migrate: :environment) do
29
30
  Apartment::TaskHelper.warn_if_tenants_empty
30
- Apartment::TaskHelper.each_tenant do |tenant|
31
+
32
+ results = Apartment::TaskHelper.each_tenant do |tenant|
31
33
  Apartment::TaskHelper.migrate_tenant(tenant)
32
34
  end
35
+
36
+ Apartment::TaskHelper.display_summary('Migration', results)
37
+
38
+ # Dump schema after successful migrations
39
+ if results.all?(&:success)
40
+ Apartment::Tasks::SchemaDumper.dump_if_enabled
41
+ else
42
+ puts '[Apartment] Skipping schema dump due to migration failures'
43
+ end
44
+
45
+ # Exit with non-zero status if any tenant failed
46
+ exit(1) if results.any? { |r| !r.success }
33
47
  end
34
48
 
35
- desc 'Seed all tenants'
36
- task :seed do
49
+ desc('Seed all tenants')
50
+ task(seed: :environment) do
37
51
  Apartment::TaskHelper.warn_if_tenants_empty
38
52
 
39
53
  Apartment::TaskHelper.each_tenant do |tenant|
@@ -47,54 +61,67 @@ apartment_namespace = namespace :apartment do
47
61
  end
48
62
  end
49
63
 
50
- desc 'Rolls the migration back to the previous version (specify steps w/ STEP=n) across all tenants.'
51
- task :rollback do
64
+ desc('Rolls the migration back to the previous version (specify steps w/ STEP=n) across all tenants.')
65
+ task(rollback: :environment) do
52
66
  Apartment::TaskHelper.warn_if_tenants_empty
53
67
 
54
- step = ENV['STEP'] ? ENV['STEP'].to_i : 1
68
+ step = ENV.fetch('STEP', '1').to_i
55
69
 
56
- Apartment::TaskHelper.each_tenant do |tenant|
70
+ results = Apartment::TaskHelper.each_tenant do |tenant|
57
71
  puts("Rolling back #{tenant} tenant")
58
- Apartment::Migrator.rollback tenant, step
59
- rescue Apartment::TenantNotFound => e
60
- puts e.message
72
+ Apartment::Migrator.rollback(tenant, step)
73
+ end
74
+
75
+ Apartment::TaskHelper.display_summary('Rollback', results)
76
+
77
+ # Dump schema after successful rollback
78
+ if results.all?(&:success)
79
+ Apartment::Tasks::SchemaDumper.dump_if_enabled
80
+ else
81
+ puts '[Apartment] Skipping schema dump due to rollback failures'
61
82
  end
83
+
84
+ exit(1) if results.any? { |r| !r.success }
62
85
  end
63
86
 
64
- namespace :migrate do
65
- desc 'Runs the "up" for a given migration VERSION across all tenants.'
66
- task :up do
87
+ namespace(:migrate) do
88
+ desc('Runs the "up" for a given migration VERSION across all tenants.')
89
+ task(up: :environment) do
67
90
  Apartment::TaskHelper.warn_if_tenants_empty
68
91
 
69
- version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
70
- raise 'VERSION is required' unless version
92
+ version = ENV.fetch('VERSION', nil)&.to_i
93
+ raise('VERSION is required') unless version
71
94
 
72
- Apartment::TaskHelper.each_tenant do |tenant|
95
+ results = Apartment::TaskHelper.each_tenant do |tenant|
73
96
  puts("Migrating #{tenant} tenant up")
74
- Apartment::Migrator.run :up, tenant, version
75
- rescue Apartment::TenantNotFound => e
76
- puts e.message
97
+ Apartment::Migrator.run(:up, tenant, version)
77
98
  end
99
+
100
+ Apartment::TaskHelper.display_summary('Migrate Up', results)
101
+ Apartment::Tasks::SchemaDumper.dump_if_enabled if results.all?(&:success)
102
+ exit(1) if results.any? { |r| !r.success }
78
103
  end
79
104
 
80
- desc 'Runs the "down" for a given migration VERSION across all tenants.'
81
- task :down do
105
+ desc('Runs the "down" for a given migration VERSION across all tenants.')
106
+ task(down: :environment) do
82
107
  Apartment::TaskHelper.warn_if_tenants_empty
83
108
 
84
- version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
85
- raise 'VERSION is required' unless version
109
+ version = ENV.fetch('VERSION', nil)&.to_i
110
+ raise('VERSION is required') unless version
86
111
 
87
- Apartment::TaskHelper.each_tenant do |tenant|
112
+ results = Apartment::TaskHelper.each_tenant do |tenant|
88
113
  puts("Migrating #{tenant} tenant down")
89
- Apartment::Migrator.run :down, tenant, version
90
- rescue Apartment::TenantNotFound => e
91
- puts e.message
114
+ Apartment::Migrator.run(:down, tenant, version)
92
115
  end
116
+
117
+ Apartment::TaskHelper.display_summary('Migrate Down', results)
118
+ Apartment::Tasks::SchemaDumper.dump_if_enabled if results.all?(&:success)
119
+ exit(1) if results.any? { |r| !r.success }
93
120
  end
94
121
 
95
- desc 'Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).'
96
- task :redo do
97
- if ENV['VERSION']
122
+ desc('Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).')
123
+ task(:redo) do
124
+ if ENV.fetch('VERSION', nil)
98
125
  apartment_namespace['migrate:down'].invoke
99
126
  apartment_namespace['migrate:up'].invoke
100
127
  else
@@ -32,9 +32,9 @@ Gem::Specification.new do |s|
32
32
 
33
33
  s.required_ruby_version = '>= 3.1'
34
34
 
35
- s.add_dependency('activerecord', '>= 6.1.0', '< 8.1')
36
- s.add_dependency('activesupport', '>= 6.1.0', '< 8.1')
35
+ s.add_dependency('activerecord', '>= 7.0.0', '< 8.2')
36
+ s.add_dependency('activesupport', '>= 7.0.0', '< 8.2')
37
37
  s.add_dependency('parallel', '< 2.0')
38
- s.add_dependency('public_suffix', '>= 2.0.5', '<= 6.0.1')
38
+ s.add_dependency('public_suffix', '>= 2.0.5', '< 7')
39
39
  s.add_dependency('rack', '>= 1.3.6', '< 4.0')
40
40
  end