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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/README.md +269 -16
- data/docs/README.md +77 -0
- data/docs/configuration.md +340 -0
- data/docs/context.md +292 -0
- data/docs/errors.md +498 -0
- data/docs/integration_testing.md +454 -0
- data/docs/migrator.md +291 -0
- data/docs/rails_integration.md +468 -0
- data/docs/schema_switcher.md +182 -0
- data/docs/tenant_resolver.md +394 -0
- data/docs/testing.md +358 -0
- data/examples/context_management.rb +198 -0
- data/examples/migration_workflow.rb +50 -0
- data/examples/rails_integration/controller_examples.rb +368 -0
- data/examples/schema_operations.rb +124 -0
- data/lib/pg_multitenant_schemas/configuration.rb +4 -4
- data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
- data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
- data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
- data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
- data/lib/pg_multitenant_schemas/migrator.rb +89 -0
- data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
- data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
- data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
- data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
- data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
- data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
- data/lib/pg_multitenant_schemas/version.rb +1 -1
- data/lib/pg_multitenant_schemas.rb +10 -5
- data/pg_multitenant_schemas.gemspec +10 -9
- data/rails_integration/app/controllers/application_controller.rb +6 -0
- data/rails_integration/app/models/tenant.rb +6 -0
- 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.
|