pg_multitenant_schemas 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +46 -0
  4. data/README.md +269 -16
  5. data/docs/README.md +77 -0
  6. data/docs/configuration.md +340 -0
  7. data/docs/context.md +292 -0
  8. data/docs/errors.md +498 -0
  9. data/docs/integration_testing.md +454 -0
  10. data/docs/migrator.md +291 -0
  11. data/docs/rails_integration.md +468 -0
  12. data/docs/schema_switcher.md +182 -0
  13. data/docs/tenant_resolver.md +394 -0
  14. data/docs/testing.md +358 -0
  15. data/examples/context_management.rb +198 -0
  16. data/examples/migration_workflow.rb +50 -0
  17. data/examples/rails_integration/controller_examples.rb +368 -0
  18. data/examples/schema_operations.rb +124 -0
  19. data/lib/pg_multitenant_schemas/configuration.rb +4 -4
  20. data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
  21. data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
  22. data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
  23. data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
  24. data/lib/pg_multitenant_schemas/migrator.rb +89 -0
  25. data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
  26. data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
  27. data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
  28. data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
  29. data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
  30. data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
  31. data/lib/pg_multitenant_schemas/version.rb +1 -1
  32. data/lib/pg_multitenant_schemas.rb +10 -5
  33. data/pg_multitenant_schemas.gemspec +10 -9
  34. data/rails_integration/app/controllers/application_controller.rb +6 -0
  35. data/rails_integration/app/models/tenant.rb +6 -0
  36. metadata +39 -17
@@ -0,0 +1,340 @@
1
+ # Configuration - Gem Settings and Options
2
+
3
+ **File**: `lib/pg_multitenant_schemas/configuration.rb`
4
+
5
+ ## 📋 Overview
6
+
7
+ The `Configuration` class manages all configurable aspects of the PG Multitenant Schemas gem. It provides a centralized way to customize behavior, set defaults, and integrate with different Rails applications.
8
+
9
+ ## 🎯 Purpose
10
+
11
+ - **Centralized Settings**: Single location for all gem configuration
12
+ - **Framework Integration**: Seamless Rails integration options
13
+ - **Customization**: Flexible options for different deployment scenarios
14
+ - **Defaults Management**: Sensible defaults with override capabilities
15
+
16
+ ## 🔧 Configuration Options
17
+
18
+ ### Core Settings
19
+
20
+ ```ruby
21
+ PgMultitenantSchemas.configure do |config|
22
+ # Default schema to use (usually 'public')
23
+ config.default_schema = 'public'
24
+
25
+ # ActiveRecord connection class
26
+ config.connection_class = 'ApplicationRecord'
27
+
28
+ # Tenant model class name
29
+ config.tenant_model = 'Tenant'
30
+
31
+ # Schema naming strategy
32
+ config.schema_prefix = nil # No prefix by default
33
+ end
34
+ ```
35
+
36
+ ### Connection Management
37
+
38
+ ```ruby
39
+ PgMultitenantSchemas.configure do |config|
40
+ # Connection class for database operations
41
+ config.connection_class = 'ApplicationRecord' # Default
42
+ # config.connection_class = 'TenantRecord' # Custom connection
43
+ # config.connection_class = 'ReadOnlyRecord' # Read-only operations
44
+ end
45
+ ```
46
+
47
+ ### Tenant Resolution
48
+
49
+ ```ruby
50
+ PgMultitenantSchemas.configure do |config|
51
+ # Model used for tenant lookup
52
+ config.tenant_model = 'Tenant' # Default
53
+ # config.tenant_model = 'Organization'
54
+ # config.tenant_model = 'Account'
55
+
56
+ # Attribute used for schema naming
57
+ config.tenant_schema_attribute = :subdomain # Default
58
+ # config.tenant_schema_attribute = :slug
59
+ # config.tenant_schema_attribute = :code
60
+ end
61
+ ```
62
+
63
+ ### Schema Management
64
+
65
+ ```ruby
66
+ PgMultitenantSchemas.configure do |config|
67
+ # Default schema for non-tenant operations
68
+ config.default_schema = 'public'
69
+
70
+ # Schema naming prefix (optional)
71
+ config.schema_prefix = 'tenant_' # Results in 'tenant_acme', 'tenant_beta'
72
+ # config.schema_prefix = nil # Results in 'acme', 'beta'
73
+
74
+ # Schema exclusion patterns
75
+ config.excluded_schemas = ['information_schema', 'pg_catalog', 'pg_toast']
76
+ end
77
+ ```
78
+
79
+ ## 🏗️ Implementation Details
80
+
81
+ ### Configuration Class Structure
82
+
83
+ ```ruby
84
+ module PgMultitenantSchemas
85
+ class Configuration
86
+ attr_accessor :default_schema,
87
+ :connection_class,
88
+ :tenant_model,
89
+ :tenant_schema_attribute,
90
+ :schema_prefix,
91
+ :excluded_schemas
92
+
93
+ def initialize
94
+ @default_schema = 'public'
95
+ @connection_class = 'ApplicationRecord'
96
+ @tenant_model = 'Tenant'
97
+ @tenant_schema_attribute = :subdomain
98
+ @schema_prefix = nil
99
+ @excluded_schemas = default_excluded_schemas
100
+ end
101
+ end
102
+ end
103
+ ```
104
+
105
+ ### Dynamic Configuration Loading
106
+
107
+ The configuration supports dynamic loading and environment-specific settings:
108
+
109
+ ```ruby
110
+ # Load from environment variables
111
+ config.default_schema = ENV['PG_MULTITENANT_DEFAULT_SCHEMA'] || 'public'
112
+
113
+ # Environment-specific configuration
114
+ if Rails.env.development?
115
+ config.schema_prefix = 'dev_'
116
+ elsif Rails.env.test?
117
+ config.schema_prefix = 'test_'
118
+ end
119
+ ```
120
+
121
+ ## 🔄 Usage Patterns
122
+
123
+ ### Basic Configuration
124
+
125
+ ```ruby
126
+ # config/initializers/pg_multitenant_schemas.rb
127
+ PgMultitenantSchemas.configure do |config|
128
+ config.default_schema = 'public'
129
+ config.tenant_model = 'Organization'
130
+ config.connection_class = 'ApplicationRecord'
131
+ end
132
+ ```
133
+
134
+ ### Advanced Configuration
135
+
136
+ ```ruby
137
+ # config/initializers/pg_multitenant_schemas.rb
138
+ PgMultitenantSchemas.configure do |config|
139
+ # Core settings
140
+ config.default_schema = 'shared'
141
+ config.tenant_model = 'Account'
142
+ config.tenant_schema_attribute = :slug
143
+
144
+ # Schema naming
145
+ config.schema_prefix = Rails.env.production? ? nil : "#{Rails.env}_"
146
+
147
+ # Connection management
148
+ config.connection_class = 'TenantRecord'
149
+
150
+ # Exclusions
151
+ config.excluded_schemas += ['analytics', 'logs']
152
+ end
153
+ ```
154
+
155
+ ### Environment-Specific Configuration
156
+
157
+ ```ruby
158
+ # config/initializers/pg_multitenant_schemas.rb
159
+ PgMultitenantSchemas.configure do |config|
160
+ case Rails.env
161
+ when 'development'
162
+ config.schema_prefix = 'dev_'
163
+ config.default_schema = 'dev_public'
164
+ when 'test'
165
+ config.schema_prefix = 'test_'
166
+ config.default_schema = 'test_public'
167
+ when 'staging'
168
+ config.schema_prefix = 'staging_'
169
+ config.default_schema = 'public'
170
+ when 'production'
171
+ config.schema_prefix = nil
172
+ config.default_schema = 'public'
173
+ end
174
+ end
175
+ ```
176
+
177
+ ## 🎛️ Configuration Validation
178
+
179
+ The gem includes configuration validation to catch common issues:
180
+
181
+ ```ruby
182
+ def validate_configuration!
183
+ raise ConfigurationError, "tenant_model must be defined" if tenant_model.blank?
184
+ raise ConfigurationError, "default_schema cannot be blank" if default_schema.blank?
185
+
186
+ # Validate tenant model exists
187
+ begin
188
+ tenant_model.constantize
189
+ rescue NameError
190
+ raise ConfigurationError, "Tenant model '#{tenant_model}' not found"
191
+ end
192
+
193
+ # Validate connection class
194
+ begin
195
+ connection_class.constantize
196
+ rescue NameError
197
+ raise ConfigurationError, "Connection class '#{connection_class}' not found"
198
+ end
199
+ end
200
+ ```
201
+
202
+ ## 🔧 Integration Examples
203
+
204
+ ### Custom Tenant Models
205
+
206
+ ```ruby
207
+ # For custom tenant model structure
208
+ class Organization < ApplicationRecord
209
+ has_many :users
210
+
211
+ def schema_name
212
+ "org_#{slug}"
213
+ end
214
+ end
215
+
216
+ # Configuration
217
+ PgMultitenantSchemas.configure do |config|
218
+ config.tenant_model = 'Organization'
219
+ config.tenant_schema_attribute = :schema_name
220
+ end
221
+ ```
222
+
223
+ ### Multiple Database Setup
224
+
225
+ ```ruby
226
+ # For applications with multiple databases
227
+ class TenantRecord < ApplicationRecord
228
+ connects_to database: { writing: :tenant_primary, reading: :tenant_replica }
229
+ end
230
+
231
+ PgMultitenantSchemas.configure do |config|
232
+ config.connection_class = 'TenantRecord'
233
+ end
234
+ ```
235
+
236
+ ### Custom Schema Naming
237
+
238
+ ```ruby
239
+ # Custom schema naming logic
240
+ module SchemaNameBuilder
241
+ def self.build_name(tenant)
242
+ case tenant.tier
243
+ when 'enterprise'
244
+ "ent_#{tenant.slug}"
245
+ when 'premium'
246
+ "prem_#{tenant.slug}"
247
+ else
248
+ "std_#{tenant.slug}"
249
+ end
250
+ end
251
+ end
252
+
253
+ PgMultitenantSchemas.configure do |config|
254
+ config.schema_name_builder = SchemaNameBuilder
255
+ end
256
+ ```
257
+
258
+ ## 🚨 Important Considerations
259
+
260
+ ### Configuration Timing
261
+
262
+ - Configure the gem **before** Rails application initialization
263
+ - Place configuration in `config/initializers/`
264
+ - Ensure configuration runs before model loading
265
+
266
+ ### Thread Safety
267
+
268
+ - Configuration is **read-only** after initialization
269
+ - Safe to access from multiple threads
270
+ - No runtime configuration changes supported
271
+
272
+ ### Performance Impact
273
+
274
+ - Configuration lookups are fast (cached)
275
+ - No database queries for configuration access
276
+ - Minimal memory overhead
277
+
278
+ ## 🔍 Configuration Debugging
279
+
280
+ ### Check Current Configuration
281
+
282
+ ```ruby
283
+ # Display current configuration
284
+ config = PgMultitenantSchemas.configuration
285
+ puts "Default Schema: #{config.default_schema}"
286
+ puts "Tenant Model: #{config.tenant_model}"
287
+ puts "Connection Class: #{config.connection_class}"
288
+ puts "Schema Prefix: #{config.schema_prefix}"
289
+ ```
290
+
291
+ ### Validate Configuration
292
+
293
+ ```ruby
294
+ # Validate configuration is correct
295
+ begin
296
+ PgMultitenantSchemas.configuration.validate_configuration!
297
+ puts "✅ Configuration is valid"
298
+ rescue PgMultitenantSchemas::ConfigurationError => e
299
+ puts "❌ Configuration error: #{e.message}"
300
+ end
301
+ ```
302
+
303
+ ## 🔗 Related Components
304
+
305
+ - **[Context](context.md)**: Uses configuration for default schema
306
+ - **[SchemaSwitcher](schema_switcher.md)**: Uses connection class configuration
307
+ - **[TenantResolver](tenant_resolver.md)**: Uses tenant model configuration
308
+ - **[Rails Integration](rails_integration.md)**: Framework-specific configuration
309
+
310
+ ## 📝 Configuration Templates
311
+
312
+ ### Standard Rails App
313
+ ```ruby
314
+ PgMultitenantSchemas.configure do |config|
315
+ config.default_schema = 'public'
316
+ config.tenant_model = 'Tenant'
317
+ config.connection_class = 'ApplicationRecord'
318
+ end
319
+ ```
320
+
321
+ ### SaaS Application
322
+ ```ruby
323
+ PgMultitenantSchemas.configure do |config|
324
+ config.default_schema = 'shared'
325
+ config.tenant_model = 'Account'
326
+ config.tenant_schema_attribute = :subdomain
327
+ config.schema_prefix = Rails.env.production? ? nil : "#{Rails.env}_"
328
+ end
329
+ ```
330
+
331
+ ### Enterprise Application
332
+ ```ruby
333
+ PgMultitenantSchemas.configure do |config|
334
+ config.default_schema = 'master'
335
+ config.tenant_model = 'Organization'
336
+ config.tenant_schema_attribute = :schema_name
337
+ config.connection_class = 'TenantRecord'
338
+ config.excluded_schemas += ['audit', 'analytics', 'logs']
339
+ end
340
+ ```
data/docs/context.md ADDED
@@ -0,0 +1,292 @@
1
+ # Context - Thread-Safe Tenant Management
2
+
3
+ **File**: `lib/pg_multitenant_schemas/context.rb`
4
+
5
+ ## 📋 Overview
6
+
7
+ The `Context` class provides thread-safe tenant context management, maintaining current tenant and schema state across request lifecycle. It's the high-level interface for tenant switching and context management.
8
+
9
+ ## 🎯 Purpose
10
+
11
+ - **Thread Safety**: Maintains separate tenant context per thread
12
+ - **Context Management**: Tracks current tenant and schema state
13
+ - **Automatic Restoration**: Ensures context is properly restored after operations
14
+ - **High-Level API**: Provides convenient methods for tenant switching
15
+
16
+ ## 🔧 Key Methods
17
+
18
+ ### Current State Management
19
+
20
+ ```ruby
21
+ # Get/Set current tenant
22
+ Context.current_tenant
23
+ Context.current_tenant = tenant_object
24
+
25
+ # Get/Set current schema
26
+ Context.current_schema
27
+ Context.current_schema = 'schema_name'
28
+
29
+ # Reset to default state
30
+ Context.reset!
31
+ ```
32
+
33
+ ### Tenant Switching
34
+
35
+ ```ruby
36
+ # Switch to specific tenant
37
+ Context.switch_to_tenant(tenant_object)
38
+
39
+ # Switch to specific schema
40
+ Context.switch_to_schema('schema_name')
41
+
42
+ # Execute block in tenant context
43
+ Context.with_tenant(tenant_or_schema) do
44
+ # Code executes in tenant context
45
+ User.all # Queries tenant's users
46
+ end
47
+ # Automatically restores previous context
48
+ ```
49
+
50
+ ### Schema Management
51
+
52
+ ```ruby
53
+ # Create tenant schema
54
+ Context.create_tenant_schema(tenant_or_schema)
55
+
56
+ # Drop tenant schema
57
+ Context.drop_tenant_schema(tenant_or_schema, cascade: true)
58
+ ```
59
+
60
+ ## 🏗️ Implementation Details
61
+
62
+ ### Thread-Local Storage
63
+
64
+ Context uses Ruby's `Thread.current` to store tenant state:
65
+
66
+ ```ruby
67
+ def current_tenant
68
+ Thread.current[:pg_multitenant_current_tenant]
69
+ end
70
+
71
+ def current_schema
72
+ Thread.current[:pg_multitenant_current_schema] ||
73
+ PgMultitenantSchemas.configuration.default_schema
74
+ end
75
+ ```
76
+
77
+ This ensures:
78
+ - **Isolation**: Each thread has independent tenant context
79
+ - **Concurrency**: Multiple requests can have different tenant contexts
80
+ - **Safety**: No cross-request tenant bleeding
81
+
82
+ ### Context Restoration
83
+
84
+ The `with_tenant` method provides automatic context restoration:
85
+
86
+ ```ruby
87
+ def with_tenant(tenant_or_schema)
88
+ # Store current state
89
+ previous_tenant = current_tenant
90
+ previous_schema = current_schema
91
+
92
+ begin
93
+ # Switch to new context
94
+ switch_to_schema(schema_name)
95
+ self.current_tenant = tenant
96
+ yield if block_given?
97
+ ensure
98
+ # Always restore previous context
99
+ restore_previous_context(previous_tenant, previous_schema)
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Flexible Input Handling
105
+
106
+ Context methods accept multiple input types:
107
+
108
+ - **Tenant Objects**: `tenant.subdomain` is used as schema name
109
+ - **String/Symbol**: Used directly as schema name
110
+ - **Nil**: Switches to default schema
111
+
112
+ ## 🔄 Usage Patterns
113
+
114
+ ### Basic Tenant Switching
115
+
116
+ ```ruby
117
+ # Using tenant object
118
+ tenant = Tenant.find_by(subdomain: 'acme')
119
+ PgMultitenantSchemas::Context.switch_to_tenant(tenant)
120
+
121
+ # Using schema name directly
122
+ PgMultitenantSchemas::Context.switch_to_schema('acme_corp')
123
+
124
+ # Check current state
125
+ puts PgMultitenantSchemas::Context.current_schema # => "acme_corp"
126
+ ```
127
+
128
+ ### Block-Based Context
129
+
130
+ ```ruby
131
+ # Execute code in tenant context
132
+ PgMultitenantSchemas::Context.with_tenant('acme_corp') do
133
+ # All database operations happen in acme_corp schema
134
+ users = User.all
135
+ orders = Order.where(status: 'pending')
136
+
137
+ # Create new records in tenant schema
138
+ User.create!(email: 'user@acme.com')
139
+ end
140
+
141
+ # Context automatically restored to previous state
142
+ puts PgMultitenantSchemas::Context.current_schema # => "public"
143
+ ```
144
+
145
+ ### Nested Contexts
146
+
147
+ ```ruby
148
+ # Nested tenant contexts work correctly
149
+ PgMultitenantSchemas::Context.with_tenant('tenant_a') do
150
+ puts "In tenant A: #{Context.current_schema}"
151
+
152
+ PgMultitenantSchemas::Context.with_tenant('tenant_b') do
153
+ puts "In tenant B: #{Context.current_schema}"
154
+ end
155
+
156
+ puts "Back in tenant A: #{Context.current_schema}"
157
+ end
158
+ puts "Back in original context: #{Context.current_schema}"
159
+ ```
160
+
161
+ ### Error Handling
162
+
163
+ ```ruby
164
+ # Context is restored even if errors occur
165
+ PgMultitenantSchemas::Context.with_tenant('acme_corp') do
166
+ raise "Something went wrong!"
167
+ rescue => e
168
+ puts "Error: #{e.message}"
169
+ end
170
+
171
+ # Context is still properly restored
172
+ puts PgMultitenantSchemas::Context.current_schema # => "public"
173
+ ```
174
+
175
+ ## 🔗 Integration Points
176
+
177
+ ### Controller Integration
178
+
179
+ ```ruby
180
+ class ApplicationController < ActionController::Base
181
+ around_action :set_tenant_context
182
+
183
+ private
184
+
185
+ def set_tenant_context
186
+ tenant = resolve_tenant_from_request
187
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
188
+ yield
189
+ end
190
+ end
191
+ end
192
+ ```
193
+
194
+ ### Background Jobs
195
+
196
+ ```ruby
197
+ class TenantJob < ApplicationJob
198
+ def perform(tenant_id, *args)
199
+ tenant = Tenant.find(tenant_id)
200
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
201
+ # Job executes in tenant context
202
+ process_tenant_data
203
+ end
204
+ end
205
+ end
206
+ ```
207
+
208
+ ### Model Callbacks
209
+
210
+ ```ruby
211
+ class Order < ApplicationRecord
212
+ after_create :send_notification
213
+
214
+ private
215
+
216
+ def send_notification
217
+ # This runs in the same tenant context as the creation
218
+ current_tenant = PgMultitenantSchemas::Context.current_tenant
219
+ NotificationMailer.order_created(self, current_tenant).deliver_later
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## ⚙️ Configuration
225
+
226
+ Context behavior is influenced by global configuration:
227
+
228
+ ```ruby
229
+ PgMultitenantSchemas.configure do |config|
230
+ config.default_schema = 'public' # Default context
231
+ config.connection_class = 'ApplicationRecord'
232
+ end
233
+ ```
234
+
235
+ ## 🚨 Important Considerations
236
+
237
+ ### Thread Safety
238
+
239
+ - **Safe**: Each thread maintains independent context
240
+ - **Isolation**: No cross-thread context interference
241
+ - **Connection Pools**: Works correctly with Rails connection pooling
242
+
243
+ ### Memory Management
244
+
245
+ - Context data is cleaned up when threads end
246
+ - No memory leaks from tenant context storage
247
+ - Minimal memory overhead per thread
248
+
249
+ ### Performance
250
+
251
+ - Context switching is very fast
252
+ - No database queries required for context management
253
+ - Minimal CPU and memory overhead
254
+
255
+ ## 🔍 Debugging
256
+
257
+ ### Check Current Context
258
+
259
+ ```ruby
260
+ # Current tenant and schema
261
+ puts "Tenant: #{PgMultitenantSchemas::Context.current_tenant&.subdomain}"
262
+ puts "Schema: #{PgMultitenantSchemas::Context.current_schema}"
263
+ ```
264
+
265
+ ### Debug Context Stack
266
+
267
+ ```ruby
268
+ # Add logging to track context changes
269
+ module PgMultitenantSchemas
270
+ class Context
271
+ class << self
272
+ alias_method :original_switch_to_schema, :switch_to_schema
273
+
274
+ def switch_to_schema(schema_name)
275
+ Rails.logger.debug "Switching to schema: #{schema_name}"
276
+ original_switch_to_schema(schema_name)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ ```
282
+
283
+ ## 🔗 Related Components
284
+
285
+ - **[SchemaSwitcher](schema_switcher.md)**: Low-level schema operations used by Context
286
+ - **[TenantResolver](tenant_resolver.md)**: Identifies tenants for context switching
287
+ - **[Rails Integration](rails_integration.md)**: Framework integration using Context
288
+ - **[Configuration](configuration.md)**: Context configuration options
289
+
290
+ ## 📝 Examples
291
+
292
+ See [examples/context_management.rb](../examples/) for complete usage examples.