pg_multitenant_schemas 0.1.3 → 0.2.2

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.actrc +17 -0
  3. data/.env.local.example +21 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +86 -0
  6. data/LOCAL_TESTING_SUMMARY.md +141 -0
  7. data/README.md +269 -16
  8. data/TESTING_LOCALLY.md +208 -0
  9. data/docs/README.md +81 -0
  10. data/docs/configuration.md +340 -0
  11. data/docs/context.md +292 -0
  12. data/docs/errors.md +498 -0
  13. data/docs/github_actions_permissions_fix.md +136 -0
  14. data/docs/github_actions_setup.md +181 -0
  15. data/docs/integration_testing.md +454 -0
  16. data/docs/local_workflow_testing.md +314 -0
  17. data/docs/migrator.md +291 -0
  18. data/docs/rails_integration.md +468 -0
  19. data/docs/schema_switcher.md +182 -0
  20. data/docs/tenant_resolver.md +394 -0
  21. data/docs/testing.md +358 -0
  22. data/examples/context_management.rb +198 -0
  23. data/examples/migration_workflow.rb +50 -0
  24. data/examples/rails_integration/controller_examples.rb +368 -0
  25. data/examples/schema_operations.rb +124 -0
  26. data/lib/pg_multitenant_schemas/configuration.rb +4 -4
  27. data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
  28. data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
  29. data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
  30. data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
  31. data/lib/pg_multitenant_schemas/migrator.rb +89 -0
  32. data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
  33. data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
  34. data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
  35. data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
  36. data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
  37. data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
  38. data/lib/pg_multitenant_schemas/version.rb +1 -1
  39. data/lib/pg_multitenant_schemas.rb +10 -5
  40. data/pg_multitenant_schemas.gemspec +10 -9
  41. data/pre-push-check.sh +95 -0
  42. data/rails_integration/app/controllers/application_controller.rb +6 -0
  43. data/rails_integration/app/models/tenant.rb +6 -0
  44. data/test-github-setup.sh +85 -0
  45. data/validate-github-commands.sh +47 -0
  46. metadata +49 -17
@@ -0,0 +1,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Rails Controller Integration Examples
4
+ # Demonstrates various controller patterns for multi-tenant applications
5
+
6
+ # Example 1: Basic Application Controller Setup
7
+ # Provides tenant resolution and access control for Rails applications
8
+ class ApplicationController < ActionController::Base
9
+ include PgMultitenantSchemas::ControllerConcern
10
+
11
+ protect_from_forgery with: :exception
12
+ before_action :authenticate_user!
13
+ before_action :ensure_tenant_access
14
+
15
+ protected
16
+
17
+ # Custom tenant resolution from request
18
+ def resolve_tenant_from_request
19
+ resolve_by_subdomain || resolve_by_domain || resolve_by_user_preference
20
+ end
21
+
22
+ private
23
+
24
+ def resolve_by_subdomain
25
+ return nil unless request.subdomain.present?
26
+
27
+ Tenant.active.find_by(subdomain: request.subdomain)
28
+ end
29
+
30
+ def resolve_by_domain
31
+ return nil unless request.domain.present?
32
+
33
+ Tenant.active.find_by(domain: request.host)
34
+ end
35
+
36
+ def resolve_by_user_preference
37
+ return nil unless user_signed_in? && current_user.current_tenant_id.present?
38
+
39
+ current_user.tenants.find_by(id: current_user.current_tenant_id)
40
+ end
41
+
42
+ def tenant_access_valid?
43
+ @current_tenant && current_user&.has_access_to?(@current_tenant)
44
+ end
45
+
46
+ def ensure_tenant_access
47
+ return handle_missing_tenant unless @current_tenant
48
+ return handle_access_denied unless tenant_access_valid?
49
+
50
+ true
51
+ end
52
+
53
+ def handle_missing_tenant
54
+ respond_to do |format|
55
+ format.html { redirect_to tenant_selection_path, alert: "Please select a tenant" }
56
+ format.json { render json: { error: "Tenant required" }, status: :unauthorized }
57
+ end
58
+ end
59
+
60
+ def handle_access_denied
61
+ respond_to do |format|
62
+ format.html { redirect_to root_path, alert: "Access denied" }
63
+ format.json { render json: { error: "Access denied" }, status: :forbidden }
64
+ end
65
+ end
66
+
67
+ # Helper method to access current tenant in views
68
+ helper_method :current_tenant
69
+ attr_reader :current_tenant
70
+
71
+ # Custom error handling for tenant-related errors
72
+ rescue_from PgMultitenantSchemas::TenantNotFoundError do |error|
73
+ Rails.logger.warn "Tenant not found: #{error.message}"
74
+
75
+ respond_to do |format|
76
+ format.html { redirect_to tenant_selection_path, alert: "Tenant not found" }
77
+ format.json { render json: { error: "Tenant not found" }, status: :not_found }
78
+ end
79
+ end
80
+
81
+ rescue_from PgMultitenantSchemas::SchemaError do |error|
82
+ Rails.logger.error "Schema error: #{error.message}"
83
+
84
+ respond_to do |format|
85
+ format.html { redirect_to root_path, alert: "A database error occurred" }
86
+ format.json { render json: { error: "Database error" }, status: :internal_server_error }
87
+ end
88
+ end
89
+ end
90
+
91
+ # Example 2: Admin Controller with Multi-Tenant Management
92
+ module Admin
93
+ # Controller for managing tenants in admin interface
94
+ # Allows administrators to view, manage, and switch between tenant contexts
95
+ class TenantsController < Admin::BaseController
96
+ # Admin controllers might need to switch between tenants
97
+
98
+ def index
99
+ # List all tenants (admin view, not tenant-scoped)
100
+ @tenants = Tenant.includes(:users, :subscriptions).order(:name)
101
+ end
102
+
103
+ def show
104
+ @tenant = Tenant.find(params[:id])
105
+
106
+ # Get tenant-specific statistics
107
+ @stats = PgMultitenantSchemas::Context.with_tenant(@tenant) do
108
+ {
109
+ users_count: User.count,
110
+ orders_count: Order.count,
111
+ revenue: Order.sum(:total),
112
+ last_activity: User.maximum(:last_sign_in_at)
113
+ }
114
+ end
115
+ end
116
+
117
+ def switch
118
+ @tenant = Tenant.find(params[:id])
119
+
120
+ # Allow admin to switch to tenant for support
121
+ if current_user.admin? && @tenant
122
+ session[:admin_impersonating_tenant] = @tenant.id
123
+ redirect_to root_path, notice: "Switched to tenant: #{@tenant.name}"
124
+ else
125
+ redirect_to admin_tenants_path, alert: "Access denied"
126
+ end
127
+ end
128
+
129
+ def stop_impersonation
130
+ session.delete(:admin_impersonating_tenant)
131
+ redirect_to admin_tenants_path, notice: "Stopped tenant impersonation"
132
+ end
133
+
134
+ private
135
+
136
+ def resolve_tenant_from_request
137
+ # Admin impersonation takes precedence
138
+ if session[:admin_impersonating_tenant] && current_user&.admin?
139
+ return Tenant.find_by(id: session[:admin_impersonating_tenant])
140
+ end
141
+
142
+ # Fall back to normal resolution
143
+ super
144
+ end
145
+ end
146
+ end
147
+
148
+ # Example 3: API Controller with JWT-based Tenant Resolution
149
+ module Api
150
+ module V1
151
+ # Base controller for API endpoints with tenant resolution
152
+ # Supports JWT and API key authentication with tenant context
153
+ class BaseController < ActionController::API
154
+ include PgMultitenantSchemas::ControllerConcern
155
+
156
+ before_action :authenticate_api_request
157
+
158
+ protected
159
+
160
+ def resolve_tenant_from_request
161
+ # Extract tenant from JWT payload
162
+ return @jwt_payload["tenant"] if @jwt_payload&.key?("tenant")
163
+
164
+ # Extract from API key
165
+ return @api_key.tenant if @api_key&.tenant
166
+
167
+ # Extract from header
168
+ tenant_id = request.headers["X-Tenant-ID"]
169
+ return Tenant.find(tenant_id) if tenant_id.present?
170
+
171
+ nil
172
+ rescue ActiveRecord::RecordNotFound
173
+ nil
174
+ end
175
+
176
+ private
177
+
178
+ def authenticate_api_request
179
+ token = extract_token_from_header
180
+
181
+ if token.present?
182
+ @jwt_payload = decode_jwt_token(token)
183
+ @api_key = ApiKey.active.find_by(token: token) if @jwt_payload.nil?
184
+ end
185
+
186
+ return if @jwt_payload || @api_key
187
+
188
+ render json: { error: "Unauthorized" }, status: :unauthorized
189
+ end
190
+
191
+ def extract_token_from_header
192
+ auth_header = request.headers["Authorization"]
193
+ auth_header&.split&.last if auth_header&.start_with?("Bearer ")
194
+ end
195
+
196
+ def decode_jwt_token(token)
197
+ JWT.decode(token, Rails.application.secret_key_base, true, algorithm: "HS256").first
198
+ rescue JWT::DecodeError, JWT::ExpiredSignature
199
+ nil
200
+ end
201
+
202
+ # API-specific error handling
203
+ rescue_from PgMultitenantSchemas::TenantNotFoundError do |error|
204
+ render json: {
205
+ error: "tenant_not_found",
206
+ message: error.message,
207
+ code: "TENANT_NOT_FOUND"
208
+ }, status: :not_found
209
+ end
210
+
211
+ rescue_from PgMultitenantSchemas::SchemaError do |_error|
212
+ render json: {
213
+ error: "schema_error",
214
+ message: "Database schema error",
215
+ code: "SCHEMA_ERROR"
216
+ }, status: :internal_server_error
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ # Example 4: Background Job Controller Integration
223
+ class JobsController < ApplicationController
224
+ def create
225
+ # Enqueue job with current tenant context
226
+ ProcessDataJob.perform_later(
227
+ current_tenant.id,
228
+ params[:data_type],
229
+ params[:options]
230
+ )
231
+
232
+ render json: {
233
+ message: "Job enqueued successfully",
234
+ tenant: current_tenant.subdomain
235
+ }
236
+ end
237
+
238
+ def status
239
+ # Check job status within tenant context
240
+ job_id = params[:job_id]
241
+
242
+ # Job status checking happens in tenant context
243
+ job_status = Rails.cache.read("job_status:#{current_tenant.id}:#{job_id}")
244
+
245
+ render json: {
246
+ job_id: job_id,
247
+ status: job_status || "not_found",
248
+ tenant: current_tenant.subdomain
249
+ }
250
+ end
251
+ end
252
+
253
+ # Example 5: Multi-Strategy Tenant Resolution Controller
254
+ class FlexibleController < ApplicationController
255
+ protected
256
+
257
+ def resolve_tenant_from_request
258
+ # Try multiple resolution strategies in order
259
+ strategies = %i[
260
+ resolve_by_custom_header
261
+ resolve_by_subdomain
262
+ resolve_by_path_parameter
263
+ resolve_by_user_preference
264
+ resolve_by_session
265
+ ]
266
+
267
+ strategies.each do |strategy|
268
+ tenant = send(strategy)
269
+ return tenant if tenant
270
+ end
271
+
272
+ nil
273
+ end
274
+
275
+ private
276
+
277
+ def resolve_by_custom_header
278
+ tenant_identifier = request.headers["X-Tenant-Identifier"]
279
+ return nil unless tenant_identifier.present?
280
+
281
+ Tenant.find_by(subdomain: tenant_identifier) ||
282
+ Tenant.find_by(slug: tenant_identifier)
283
+ end
284
+
285
+ def resolve_by_subdomain
286
+ return nil unless request.subdomain.present?
287
+
288
+ Tenant.find_by(subdomain: request.subdomain)
289
+ end
290
+
291
+ def resolve_by_path_parameter
292
+ # For routes like /t/:tenant_slug/dashboard
293
+ tenant_slug = params[:tenant_slug]
294
+ return nil unless tenant_slug.present?
295
+
296
+ Tenant.find_by(slug: tenant_slug)
297
+ end
298
+
299
+ def resolve_by_user_preference
300
+ return nil unless user_signed_in?
301
+
302
+ tenant_id = current_user.preferred_tenant_id
303
+ current_user.tenants.find_by(id: tenant_id) if tenant_id.present?
304
+ end
305
+
306
+ def resolve_by_session
307
+ tenant_id = session[:selected_tenant_id]
308
+ Tenant.find_by(id: tenant_id) if tenant_id.present?
309
+ end
310
+ end
311
+
312
+ # Example 6: WebSocket Integration
313
+ module ApplicationCable
314
+ # WebSocket connection with tenant context support
315
+ # Establishes tenant context for real-time communication
316
+ class Connection < ActionCable::Connection::Base
317
+ identified_by :current_user, :current_tenant
318
+
319
+ def connect
320
+ self.current_user = find_verified_user
321
+ self.current_tenant = find_tenant_for_user
322
+
323
+ # Set tenant context for the connection
324
+ PgMultitenantSchemas::Context.switch_to_tenant(current_tenant) if current_tenant
325
+ end
326
+
327
+ private
328
+
329
+ def find_verified_user
330
+ if (verified_user = User.find_by(id: cookies.signed[:user_id]))
331
+ verified_user
332
+ else
333
+ reject_unauthorized_connection
334
+ end
335
+ end
336
+
337
+ def find_tenant_for_user
338
+ # Extract tenant from connection params or user preferences
339
+ tenant_id = request.params[:tenant_id] || current_user.current_tenant_id
340
+ current_user.tenants.find_by(id: tenant_id)
341
+ end
342
+ end
343
+ end
344
+
345
+ # Example WebSocket Channel with Tenant Context
346
+ class NotificationsChannel < ApplicationCable::Channel
347
+ def subscribed
348
+ # Subscribe to tenant-specific notifications
349
+ if current_tenant
350
+ stream_from "notifications_#{current_tenant.id}"
351
+ else
352
+ reject
353
+ end
354
+ end
355
+
356
+ def receive(data)
357
+ # All operations happen within tenant context
358
+ PgMultitenantSchemas::Context.with_tenant(current_tenant) do
359
+ case data["action"]
360
+ when "mark_as_read"
361
+ notification = Notification.find(data["notification_id"])
362
+ notification.mark_as_read!
363
+ when "create"
364
+ Notification.create!(data["notification"])
365
+ end
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Schema Operations Example
5
+ # Demonstrates core schema switching and management operations
6
+
7
+ require_relative "../lib/pg_multitenant_schemas"
8
+
9
+ puts "šŸ”§ PG Multitenant Schemas - Schema Operations Example"
10
+ puts "====================================================="
11
+
12
+ # Example 1: Basic Schema Switching
13
+ puts "\nšŸ“‹ Example 1: Basic Schema Switching"
14
+ puts "------------------------------------"
15
+
16
+ # Switch to a tenant schema
17
+ schema_name = "demo_tenant"
18
+ puts "Switching to schema: #{schema_name}"
19
+
20
+ begin
21
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema(schema_name)
22
+ current = PgMultitenantSchemas::SchemaSwitcher.current_schema
23
+ puts "āœ… Current schema: #{current}"
24
+ rescue PgMultitenantSchemas::SchemaError => e
25
+ puts "āŒ Schema switch failed: #{e.message}"
26
+ end
27
+
28
+ # Example 2: Schema Creation and Management
29
+ puts "\nšŸ—ļø Example 2: Schema Creation and Management"
30
+ puts "---------------------------------------------"
31
+
32
+ new_schema = "example_tenant"
33
+ puts "Creating schema: #{new_schema}"
34
+
35
+ begin
36
+ # Create new schema
37
+ PgMultitenantSchemas::SchemaSwitcher.create_schema(new_schema)
38
+ puts "āœ… Schema created: #{new_schema}"
39
+
40
+ # Verify schema exists
41
+ puts "āœ… Schema exists: #{new_schema}" if PgMultitenantSchemas::SchemaSwitcher.schema_exists?(new_schema)
42
+
43
+ # List all schemas
44
+ schemas = PgMultitenantSchemas::SchemaSwitcher.list_schemas
45
+ puts "šŸ“‹ Available schemas: #{schemas.join(", ")}"
46
+ rescue PgMultitenantSchemas::SchemaError => e
47
+ puts "āŒ Schema operation failed: #{e.message}"
48
+ end
49
+
50
+ # Example 3: Safe Schema Operations
51
+ puts "\nšŸ›”ļø Example 3: Safe Schema Operations"
52
+ puts "------------------------------------"
53
+
54
+ begin
55
+ # Store current schema
56
+ original_schema = PgMultitenantSchemas::SchemaSwitcher.current_schema
57
+ puts "Original schema: #{original_schema}"
58
+
59
+ # Switch to tenant schema
60
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema("demo_tenant")
61
+ puts "Switched to: #{PgMultitenantSchemas::SchemaSwitcher.current_schema}"
62
+
63
+ # Execute some operations here...
64
+ puts "Performing tenant-specific operations..."
65
+
66
+ # Always restore original schema
67
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema(original_schema)
68
+ puts "āœ… Restored to original schema: #{PgMultitenantSchemas::SchemaSwitcher.current_schema}"
69
+ rescue PgMultitenantSchemas::SchemaError => e
70
+ puts "āŒ Error: #{e.message}"
71
+ # Ensure we restore the original schema even on error
72
+ begin
73
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema(original_schema)
74
+ rescue StandardError
75
+ nil
76
+ end
77
+ end
78
+
79
+ # Example 4: Schema Validation
80
+ puts "\nāœ… Example 4: Schema Validation"
81
+ puts "-------------------------------"
82
+
83
+ test_schemas = ["valid_schema", "invalid-schema!", "", "another_valid_123"]
84
+
85
+ test_schemas.each do |schema|
86
+ puts "Testing schema name: '#{schema}'"
87
+
88
+ if schema.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/) && !schema.blank?
89
+ puts " āœ… Valid schema name"
90
+
91
+ # Try to create (for demonstration)
92
+ # PgMultitenantSchemas::SchemaSwitcher.create_schema(schema)
93
+ else
94
+ puts " āŒ Invalid schema name format"
95
+ end
96
+ rescue PgMultitenantSchemas::SchemaError => e
97
+ puts " āŒ Schema error: #{e.message}"
98
+ end
99
+
100
+ # Example 5: Schema Cleanup
101
+ puts "\n🧹 Example 5: Schema Cleanup"
102
+ puts "----------------------------"
103
+
104
+ cleanup_schema = "example_tenant"
105
+ puts "Cleaning up schema: #{cleanup_schema}"
106
+
107
+ begin
108
+ if PgMultitenantSchemas::SchemaSwitcher.schema_exists?(cleanup_schema)
109
+ PgMultitenantSchemas::SchemaSwitcher.drop_schema(cleanup_schema)
110
+ puts "āœ… Schema dropped: #{cleanup_schema}"
111
+ else
112
+ puts "ā„¹ļø Schema doesn't exist: #{cleanup_schema}"
113
+ end
114
+ rescue PgMultitenantSchemas::SchemaError => e
115
+ puts "āŒ Cleanup failed: #{e.message}"
116
+ end
117
+
118
+ puts "\n✨ Schema operations example completed!"
119
+ puts "\nšŸ“– Key Takeaways:"
120
+ puts " • Always validate schema names before operations"
121
+ puts " • Use proper error handling for schema operations"
122
+ puts " • Store and restore original schema in ensure blocks"
123
+ puts " • Check schema existence before creation or deletion"
124
+ puts " • Use meaningful schema naming conventions"
@@ -13,11 +13,11 @@ module PgMultitenantSchemas
13
13
  @tenant_model_class = "Tenant"
14
14
  @default_schema = "public"
15
15
  @development_fallback = false
16
- @excluded_subdomains = %w[www api admin mail ftp blog support help docs]
17
- @common_tlds = %w[com org net edu gov mil int co uk ca au de fr jp cn]
16
+ @excluded_subdomains = %w[www api admin mail ftp blog support help docs staging]
17
+ @common_tlds = %w[com org net edu gov mil int co uk ca au de fr jp cn dev test]
18
18
  @auto_create_schemas = true
19
- @connection_class = "ActiveRecord::Base"
20
- @logger = nil
19
+ @connection_class = "ApplicationRecord"
20
+ @logger = defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
21
21
  end
22
22
 
23
23
  def tenant_model
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Module for migration display and reporting functionality
5
+ # Handles console output and summary reports for migration operations
6
+ module MigrationDisplayReporter
7
+ private
8
+
9
+ def display_migration_summary(results, verbose)
10
+ return unless verbose
11
+
12
+ summary = results.group_by { |r| r[:status] }.transform_values(&:count)
13
+
14
+ puts "\nšŸ“Š Migration Summary:"
15
+ puts " āœ… Successful: #{summary[:success] || 0}"
16
+ puts " āŒ Failed: #{summary[:error] || 0}"
17
+ puts " ā­ļø Skipped: #{summary[:skipped] || 0}"
18
+ end
19
+
20
+ def display_setup_summary(results, verbose)
21
+ return unless verbose
22
+
23
+ summary = results.group_by { |r| r[:status] }.transform_values(&:count)
24
+
25
+ puts "\nšŸ“Š Setup Summary:"
26
+ puts " āœ… Successful: #{summary[:success] || 0}"
27
+ puts " āŒ Failed: #{summary[:error] || 0}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Module for executing migration operations
5
+ # Handles the core migration execution and error handling logic
6
+ module MigrationExecutor
7
+ private
8
+
9
+ def process_schemas_migration(schemas, verbose, ignore_errors)
10
+ puts "šŸš€ Starting migrations for #{schemas.count} tenant schemas..." if verbose
11
+
12
+ schemas.map do |schema|
13
+ migrate_tenant(schema, verbose: verbose, raise_on_error: !ignore_errors)
14
+ end
15
+ end
16
+
17
+ def handle_missing_schema(schema_name, verbose, raise_on_error)
18
+ message = "Schema '#{schema_name}' does not exist"
19
+ puts " āš ļø #{message}" if verbose
20
+ raise StandardError, message if raise_on_error
21
+
22
+ { schema: schema_name, status: :skipped, message: message }
23
+ end
24
+
25
+ def execute_tenant_migration(schema_name, verbose, raise_on_error)
26
+ puts " šŸ“¦ Migrating schema: #{schema_name}" if verbose
27
+ original_schema = current_schema
28
+
29
+ begin
30
+ result = perform_migration_for_schema(schema_name, verbose)
31
+ puts " āœ… Migration completed" if verbose && result[:status] == :success
32
+ result
33
+ rescue StandardError => e
34
+ handle_migration_error(schema_name, e, verbose, raise_on_error)
35
+ ensure
36
+ switch_to_schema(original_schema) if original_schema
37
+ end
38
+ end
39
+
40
+ def perform_migration_for_schema(schema_name, verbose)
41
+ switch_to_schema(schema_name)
42
+ pending_count = pending_migrations.count
43
+
44
+ if pending_count.zero?
45
+ puts " ā„¹ļø No pending migrations" if verbose
46
+ return { schema: schema_name, status: :success, message: "No pending migrations" }
47
+ end
48
+
49
+ run_migrations
50
+ { schema: schema_name, status: :success, message: "#{pending_count} migrations applied" }
51
+ end
52
+
53
+ def handle_migration_error(schema_name, error, verbose, raise_on_error)
54
+ message = "Migration failed: #{error.message}"
55
+ puts " āŒ #{message}" if verbose
56
+ raise error if raise_on_error
57
+
58
+ { schema: schema_name, status: :error, message: message }
59
+ end
60
+
61
+ def create_tenant_schema_if_needed(schema_name, verbose)
62
+ if schema_exists?(schema_name)
63
+ puts " ā„¹ļø Schema already exists" if verbose
64
+ else
65
+ create_schema(schema_name)
66
+ puts " āœ… Schema created" if verbose
67
+ end
68
+ end
69
+
70
+ def validate_tenant_model_exists
71
+ raise "Tenant model not found. Please define a Tenant model." unless defined?(Tenant)
72
+ end
73
+
74
+ def process_setup_for_tenants(tenants, verbose)
75
+ tenants.map do |tenant|
76
+ schema_name = extract_schema_name(tenant)
77
+ setup_tenant(schema_name, verbose: verbose)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Schema operations for migration management
5
+ module MigrationSchemaOperations
6
+ private
7
+
8
+ # Delegate schema operations to SchemaSwitcher
9
+ def switch_to_schema(schema_name)
10
+ SchemaSwitcher.switch_schema(schema_name)
11
+ end
12
+
13
+ def create_schema(schema_name)
14
+ SchemaSwitcher.create_schema(schema_name)
15
+ end
16
+
17
+ def schema_exists?(schema_name)
18
+ SchemaSwitcher.schema_exists?(schema_name)
19
+ end
20
+
21
+ def current_schema
22
+ SchemaSwitcher.current_schema
23
+ end
24
+
25
+ def tenant_schemas
26
+ SchemaSwitcher.list_schemas.reject do |schema|
27
+ %w[information_schema pg_catalog public pg_temp_1 pg_toast_temp_1].include?(schema) ||
28
+ schema.start_with?("pg_toast")
29
+ end
30
+ end
31
+
32
+ def extract_schema_name(tenant)
33
+ tenant.respond_to?(:subdomain) ? tenant.subdomain : tenant.to_s
34
+ end
35
+
36
+ def run_migrations
37
+ ActiveRecord::Base.connection.migrate
38
+ end
39
+
40
+ def pending_migrations
41
+ ActiveRecord::Base.connection.migration_context.migrations.reject do |migration|
42
+ ActiveRecord::Base.connection.migration_context.get_all_versions.include?(migration.version)
43
+ end
44
+ end
45
+
46
+ def applied_migrations
47
+ ActiveRecord::Base.connection.migration_context.get_all_versions
48
+ end
49
+
50
+ def migration_paths
51
+ ActiveRecord::Base.connection.migration_context.migrations_paths
52
+ end
53
+ end
54
+ end