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,468 @@
1
+ # Rails Integration - Framework Components
2
+
3
+ **Files**: `lib/pg_multitenant_schemas/rails/`
4
+
5
+ ## 📋 Overview
6
+
7
+ The Rails integration components provide seamless integration with the Ruby on Rails framework, including controller concerns, model concerns, and automatic configuration through Railtie.
8
+
9
+ ## 🎯 Purpose
10
+
11
+ - **Framework Integration**: Native Rails framework support
12
+ - **Controller Concerns**: Automatic tenant resolution in controllers
13
+ - **Model Concerns**: Tenant-aware model behavior
14
+ - **Automatic Setup**: Zero-configuration Rails integration
15
+ - **Middleware Support**: Request-level tenant handling
16
+
17
+ ## 🔧 Core Components
18
+
19
+ ### 1. Controller Concern (`controller_concern.rb`)
20
+
21
+ Provides automatic tenant resolution and context switching for Rails controllers.
22
+
23
+ ```ruby
24
+ module PgMultitenantSchemas
25
+ module ControllerConcern
26
+ extend ActiveSupport::Concern
27
+
28
+ included do
29
+ before_action :set_tenant_context
30
+ around_action :with_tenant_context
31
+ end
32
+
33
+ private
34
+
35
+ def set_tenant_context
36
+ @current_tenant = resolve_tenant_from_request
37
+ end
38
+
39
+ def with_tenant_context
40
+ if @current_tenant
41
+ PgMultitenantSchemas::Context.with_tenant(@current_tenant) do
42
+ yield
43
+ end
44
+ else
45
+ yield
46
+ end
47
+ end
48
+ end
49
+ end
50
+ ```
51
+
52
+ ### 2. Model Concern (`model_concern.rb`)
53
+
54
+ Adds tenant-aware behavior to ActiveRecord models.
55
+
56
+ ```ruby
57
+ module PgMultitenantSchemas
58
+ module ModelConcern
59
+ extend ActiveSupport::Concern
60
+
61
+ included do
62
+ scope :current_tenant, -> {
63
+ # Already scoped by schema context
64
+ all
65
+ }
66
+ end
67
+
68
+ class_methods do
69
+ def tenant_scoped?
70
+ true
71
+ end
72
+
73
+ def create_for_tenant!(tenant, attributes = {})
74
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
75
+ create!(attributes)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ ```
82
+
83
+ ### 3. Railtie (`railtie.rb`)
84
+
85
+ Automatic Rails integration and configuration.
86
+
87
+ ```ruby
88
+ module PgMultitenantSchemas
89
+ class Railtie < Rails::Railtie
90
+ initializer "pg_multitenant_schemas.load_tasks" do
91
+ load "pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake"
92
+ end
93
+
94
+ initializer "pg_multitenant_schemas.active_record" do
95
+ ActiveSupport.on_load(:active_record) do
96
+ # Auto-include model concern for ApplicationRecord
97
+ include PgMultitenantSchemas::ModelConcern if self == ApplicationRecord
98
+ end
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ ## 🔄 Usage Patterns
105
+
106
+ ### Controller Integration
107
+
108
+ #### Basic Setup
109
+
110
+ ```ruby
111
+ class ApplicationController < ActionController::Base
112
+ include PgMultitenantSchemas::ControllerConcern
113
+
114
+ private
115
+
116
+ def resolve_tenant_from_request
117
+ # Custom tenant resolution logic
118
+ subdomain = request.subdomain
119
+ Tenant.find_by(subdomain: subdomain) if subdomain.present?
120
+ end
121
+ end
122
+ ```
123
+
124
+ #### Advanced Controller Setup
125
+
126
+ ```ruby
127
+ class ApplicationController < ActionController::Base
128
+ include PgMultitenantSchemas::ControllerConcern
129
+
130
+ before_action :authenticate_user!
131
+ before_action :ensure_tenant_access
132
+
133
+ private
134
+
135
+ def resolve_tenant_from_request
136
+ # Multi-strategy tenant resolution
137
+ tenant = resolve_by_subdomain || resolve_by_header || resolve_by_session
138
+
139
+ # Ensure user has access to tenant
140
+ tenant if tenant && current_user&.has_access_to?(tenant)
141
+ end
142
+
143
+ def resolve_by_subdomain
144
+ subdomain = request.subdomain
145
+ Tenant.active.find_by(subdomain: subdomain) if subdomain.present?
146
+ end
147
+
148
+ def resolve_by_header
149
+ tenant_id = request.headers['X-Tenant-ID']
150
+ Tenant.find(tenant_id) if tenant_id.present?
151
+ rescue ActiveRecord::RecordNotFound
152
+ nil
153
+ end
154
+
155
+ def resolve_by_session
156
+ tenant_id = session[:tenant_id]
157
+ Tenant.find(tenant_id) if tenant_id.present?
158
+ rescue ActiveRecord::RecordNotFound
159
+ nil
160
+ end
161
+
162
+ def ensure_tenant_access
163
+ unless @current_tenant
164
+ respond_to do |format|
165
+ format.html { redirect_to tenant_selection_path }
166
+ format.json { render json: { error: 'Tenant required' }, status: :unauthorized }
167
+ end
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ #### API Controller Integration
174
+
175
+ ```ruby
176
+ class Api::BaseController < ActionController::API
177
+ include PgMultitenantSchemas::ControllerConcern
178
+
179
+ before_action :authenticate_api_request
180
+
181
+ private
182
+
183
+ def resolve_tenant_from_request
184
+ # Extract tenant from JWT or API key
185
+ if @api_token
186
+ @api_token.tenant
187
+ elsif @jwt_payload
188
+ Tenant.find(@jwt_payload['tenant_id'])
189
+ end
190
+ end
191
+
192
+ def authenticate_api_request
193
+ token = request.headers['Authorization']&.split(' ')&.last
194
+
195
+ if token.present?
196
+ @api_token = ApiToken.active.find_by(token: token)
197
+ @jwt_payload = decode_jwt(token) if @api_token.nil?
198
+ end
199
+
200
+ head :unauthorized unless @api_token || @jwt_payload
201
+ end
202
+ end
203
+ ```
204
+
205
+ ### Model Integration
206
+
207
+ #### Basic Model Setup
208
+
209
+ ```ruby
210
+ class ApplicationRecord < ActiveRecord::Base
211
+ include PgMultitenantSchemas::ModelConcern
212
+
213
+ self.abstract_class = true
214
+ end
215
+
216
+ # All models automatically inherit tenant awareness
217
+ class User < ApplicationRecord
218
+ # Automatically tenant-scoped through schema context
219
+ end
220
+
221
+ class Order < ApplicationRecord
222
+ belongs_to :user
223
+
224
+ # Custom tenant-aware scopes
225
+ scope :recent, -> { where('created_at > ?', 1.week.ago) }
226
+ scope :for_tenant, ->(tenant) {
227
+ PgMultitenantSchemas::Context.with_tenant(tenant) { all }
228
+ }
229
+ end
230
+ ```
231
+
232
+ #### Advanced Model Patterns
233
+
234
+ ```ruby
235
+ class TenantAwareRecord < ApplicationRecord
236
+ include PgMultitenantSchemas::ModelConcern
237
+
238
+ self.abstract_class = true
239
+
240
+ # Callbacks for tenant operations
241
+ before_create :set_tenant_metadata
242
+ after_create :notify_tenant_activity
243
+
244
+ class_methods do
245
+ def create_for_all_tenants!(attributes = {})
246
+ results = []
247
+
248
+ Tenant.active.find_each do |tenant|
249
+ result = create_for_tenant!(tenant, attributes)
250
+ results << result
251
+ end
252
+
253
+ results
254
+ end
255
+
256
+ def migrate_to_tenant(source_tenant, target_tenant, record_id)
257
+ source_record = nil
258
+
259
+ # Get record from source tenant
260
+ PgMultitenantSchemas::Context.with_tenant(source_tenant) do
261
+ source_record = find(record_id)
262
+ end
263
+
264
+ # Create in target tenant
265
+ PgMultitenantSchemas::Context.with_tenant(target_tenant) do
266
+ create!(source_record.attributes.except('id', 'created_at'))
267
+ end
268
+ end
269
+ end
270
+
271
+ private
272
+
273
+ def set_tenant_metadata
274
+ if PgMultitenantSchemas::Context.current_tenant
275
+ self.tenant_id = PgMultitenantSchemas::Context.current_tenant.id
276
+ end
277
+ end
278
+
279
+ def notify_tenant_activity
280
+ TenantActivityJob.perform_later(
281
+ PgMultitenantSchemas::Context.current_tenant&.id,
282
+ self.class.name,
283
+ 'created'
284
+ )
285
+ end
286
+ end
287
+ ```
288
+
289
+ ### Background Job Integration
290
+
291
+ ```ruby
292
+ class TenantJob < ApplicationJob
293
+ include PgMultitenantSchemas::ControllerConcern
294
+
295
+ def perform(tenant_id, *args)
296
+ tenant = Tenant.find(tenant_id)
297
+
298
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
299
+ process_tenant_specific_work(*args)
300
+ end
301
+ end
302
+
303
+ private
304
+
305
+ def process_tenant_specific_work(*args)
306
+ # All database operations automatically scoped to tenant
307
+ users = User.all
308
+ orders = Order.pending
309
+
310
+ # Process tenant-specific data
311
+ end
312
+ end
313
+ ```
314
+
315
+ ## 🎛️ Configuration and Customization
316
+
317
+ ### Custom Controller Concern
318
+
319
+ ```ruby
320
+ module CustomTenantController
321
+ extend ActiveSupport::Concern
322
+
323
+ include PgMultitenantSchemas::ControllerConcern
324
+
325
+ included do
326
+ before_action :log_tenant_access
327
+ after_action :track_tenant_activity
328
+ end
329
+
330
+ private
331
+
332
+ def log_tenant_access
333
+ if @current_tenant
334
+ Rails.logger.info "Tenant access: #{@current_tenant.subdomain} by #{current_user&.email}"
335
+ end
336
+ end
337
+
338
+ def track_tenant_activity
339
+ TenantAnalytics.track_request(
340
+ tenant: @current_tenant,
341
+ user: current_user,
342
+ action: "#{controller_name}##{action_name}",
343
+ ip: request.remote_ip
344
+ )
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Custom Model Behavior
350
+
351
+ ```ruby
352
+ module CustomTenantModel
353
+ extend ActiveSupport::Concern
354
+
355
+ include PgMultitenantSchemas::ModelConcern
356
+
357
+ included do
358
+ # Add audit fields
359
+ before_save :set_tenant_audit_fields
360
+
361
+ # Add tenant validation
362
+ validate :ensure_tenant_context
363
+ end
364
+
365
+ class_methods do
366
+ def tenant_report(tenant)
367
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
368
+ {
369
+ total_records: count,
370
+ recent_records: where('created_at > ?', 1.week.ago).count,
371
+ tenant_name: tenant.name
372
+ }
373
+ end
374
+ end
375
+ end
376
+
377
+ private
378
+
379
+ def set_tenant_audit_fields
380
+ current_tenant = PgMultitenantSchemas::Context.current_tenant
381
+
382
+ if current_tenant
383
+ self.tenant_context_id = current_tenant.id
384
+ self.tenant_context_name = current_tenant.subdomain
385
+ end
386
+ end
387
+
388
+ def ensure_tenant_context
389
+ unless PgMultitenantSchemas::Context.current_tenant
390
+ errors.add(:base, 'Must be created within tenant context')
391
+ end
392
+ end
393
+ end
394
+ ```
395
+
396
+ ### Middleware Integration
397
+
398
+ ```ruby
399
+ class TenantMiddleware
400
+ def initialize(app)
401
+ @app = app
402
+ end
403
+
404
+ def call(env)
405
+ request = ActionDispatch::Request.new(env)
406
+
407
+ # Resolve tenant early in request cycle
408
+ tenant = resolve_tenant(request)
409
+
410
+ if tenant
411
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
412
+ @app.call(env)
413
+ end
414
+ else
415
+ handle_no_tenant(env)
416
+ end
417
+ end
418
+
419
+ private
420
+
421
+ def resolve_tenant(request)
422
+ # Implementation depends on tenant resolution strategy
423
+ subdomain = request.subdomain
424
+ Tenant.find_by(subdomain: subdomain) if subdomain.present?
425
+ end
426
+
427
+ def handle_no_tenant(env)
428
+ # Return appropriate response for missing tenant
429
+ [404, {'Content-Type' => 'text/plain'}, ['Tenant not found']]
430
+ end
431
+ end
432
+ ```
433
+
434
+ ## 🚨 Important Considerations
435
+
436
+ ### Performance
437
+
438
+ - **Connection Pooling**: Ensure proper connection pool sizing for multi-tenant load
439
+ - **Query Optimization**: Schema switching doesn't add query overhead
440
+ - **Caching**: Implement tenant-aware caching strategies
441
+
442
+ ### Security
443
+
444
+ - **Tenant Isolation**: Verify complete data isolation between tenants
445
+ - **Access Control**: Implement proper tenant access controls
446
+ - **Audit Logging**: Log tenant access and data changes
447
+
448
+ ### Error Handling
449
+
450
+ - **Graceful Degradation**: Handle tenant resolution failures gracefully
451
+ - **Error Reporting**: Include tenant context in error reports
452
+ - **Fallback Strategies**: Implement fallback for tenant resolution failures
453
+
454
+ ## 🔗 Related Components
455
+
456
+ - **[Context](context.md)**: Core tenant context management used by Rails components
457
+ - **[TenantResolver](tenant_resolver.md)**: Tenant resolution strategies for controllers
458
+ - **[Configuration](configuration.md)**: Rails integration configuration options
459
+ - **[Migrator](migrator.md)**: Database migration management for Rails apps
460
+
461
+ ## 📝 Examples
462
+
463
+ See [examples/rails_integration/](../examples/rails_integration/) for complete Rails integration examples including:
464
+ - Controller setup patterns
465
+ - Model integration examples
466
+ - Background job handling
467
+ - Middleware implementation
468
+ - API authentication strategies
@@ -0,0 +1,182 @@
1
+ # Schema Switcher - Core PostgreSQL Operations
2
+
3
+ **File**: `lib/pg_multitenant_schemas/schema_switcher.rb`
4
+
5
+ ## 📋 Overview
6
+
7
+ The `SchemaSwitcher` is the foundation of the multitenancy system, providing low-level PostgreSQL schema operations. It handles the actual database schema switching, creation, and management.
8
+
9
+ ## 🎯 Purpose
10
+
11
+ - **Schema Switching**: Changes the PostgreSQL search path to target specific tenant schemas
12
+ - **Schema Management**: Creates, drops, and manages PostgreSQL schemas
13
+ - **Connection Handling**: Works with both Rails connections and raw PG connections
14
+ - **SQL Execution**: Provides safe SQL execution within schema contexts
15
+
16
+ ## 🔧 Key Methods
17
+
18
+ ### Core Operations
19
+
20
+ ```ruby
21
+ # Switch to a specific schema
22
+ SchemaSwitcher.switch_schema(schema_name)
23
+
24
+ # Create a new schema
25
+ SchemaSwitcher.create_schema(schema_name)
26
+
27
+ # Drop an existing schema
28
+ SchemaSwitcher.drop_schema(schema_name, cascade: true)
29
+
30
+ # Execute SQL in current schema context
31
+ SchemaSwitcher.execute_sql(sql_statement)
32
+ ```
33
+
34
+ ### Schema Introspection
35
+
36
+ ```ruby
37
+ # Check if schema exists
38
+ SchemaSwitcher.schema_exists?(schema_name)
39
+
40
+ # Get current schema name
41
+ SchemaSwitcher.current_schema
42
+
43
+ # List all schemas
44
+ SchemaSwitcher.list_schemas
45
+ ```
46
+
47
+ ## 🏗️ Implementation Details
48
+
49
+ ### Connection Management
50
+
51
+ The SchemaSwitcher works with multiple connection types:
52
+
53
+ - **Rails ActiveRecord**: Uses `ActiveRecord::Base.connection`
54
+ - **Raw PG Connection**: Direct PostgreSQL connections
55
+ - **Connection Pooling**: Thread-safe connection handling
56
+
57
+ ### Schema Path Management
58
+
59
+ PostgreSQL uses a search path to determine schema precedence:
60
+
61
+ ```sql
62
+ -- Default search path
63
+ SET search_path TO tenant_schema, public;
64
+
65
+ -- This allows:
66
+ -- 1. Tenant-specific tables in tenant_schema
67
+ -- 2. Fallback to shared tables in public schema
68
+ ```
69
+
70
+ ### Error Handling
71
+
72
+ The SchemaSwitcher provides robust error handling:
73
+
74
+ - **Invalid Schema Names**: Validates schema name format
75
+ - **Connection Errors**: Handles database connection issues
76
+ - **Permission Errors**: Manages PostgreSQL permission problems
77
+ - **Transaction Safety**: Ensures schema switches are transaction-safe
78
+
79
+ ## 🔄 Usage Patterns
80
+
81
+ ### Basic Schema Switching
82
+
83
+ ```ruby
84
+ # Switch to tenant schema
85
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema('acme_corp')
86
+
87
+ # Execute queries - now in acme_corp schema
88
+ User.all # Queries acme_corp.users table
89
+
90
+ # Switch back to public
91
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema('public')
92
+ ```
93
+
94
+ ### Schema Creation Workflow
95
+
96
+ ```ruby
97
+ # Create new tenant schema
98
+ schema_name = 'new_tenant'
99
+ PgMultitenantSchemas::SchemaSwitcher.create_schema(schema_name)
100
+
101
+ # Switch to new schema
102
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema(schema_name)
103
+
104
+ # Run migrations in new schema
105
+ ActiveRecord::Tasks::DatabaseTasks.migrate
106
+ ```
107
+
108
+ ### Safe SQL Execution
109
+
110
+ ```ruby
111
+ # Execute custom SQL in current schema
112
+ sql = "CREATE INDEX CONCURRENTLY idx_users_email ON users(email);"
113
+ PgMultitenantSchemas::SchemaSwitcher.execute_sql(sql)
114
+ ```
115
+
116
+ ## ⚙️ Configuration
117
+
118
+ The SchemaSwitcher respects global configuration:
119
+
120
+ ```ruby
121
+ PgMultitenantSchemas.configure do |config|
122
+ config.default_schema = 'public'
123
+ config.connection_class = 'ApplicationRecord'
124
+ end
125
+ ```
126
+
127
+ ## 🚨 Important Considerations
128
+
129
+ ### Thread Safety
130
+
131
+ - Schema switching is **per-connection**
132
+ - Each thread maintains its own schema context
133
+ - Connection pooling ensures thread isolation
134
+
135
+ ### Transaction Behavior
136
+
137
+ - Schema switches are **not transactional**
138
+ - Schema changes persist beyond transaction boundaries
139
+ - Always restore previous schema in ensure blocks
140
+
141
+ ### Performance
142
+
143
+ - Schema switching is fast (single SQL command)
144
+ - No data copying or migration required
145
+ - Minimal overhead for tenant switching
146
+
147
+ ## 🔍 Debugging
148
+
149
+ ### Check Current Schema
150
+
151
+ ```ruby
152
+ current = PgMultitenantSchemas::SchemaSwitcher.current_schema
153
+ puts "Currently in schema: #{current}"
154
+ ```
155
+
156
+ ### Verify Schema Exists
157
+
158
+ ```ruby
159
+ if PgMultitenantSchemas::SchemaSwitcher.schema_exists?('tenant_name')
160
+ puts "Schema exists"
161
+ else
162
+ puts "Schema not found"
163
+ end
164
+ ```
165
+
166
+ ### List All Schemas
167
+
168
+ ```ruby
169
+ schemas = PgMultitenantSchemas::SchemaSwitcher.list_schemas
170
+ puts "Available schemas: #{schemas.join(', ')}"
171
+ ```
172
+
173
+ ## 🔗 Related Components
174
+
175
+ - **[Context](context.md)**: High-level tenant context management
176
+ - **[Migrator](migrator.md)**: Migration management using SchemaSwitcher
177
+ - **[Configuration](configuration.md)**: SchemaSwitcher configuration options
178
+ - **[Errors](errors.md)**: Schema-related error handling
179
+
180
+ ## 📝 Examples
181
+
182
+ See [examples/schema_operations.rb](../examples/) for complete usage examples.