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
data/docs/errors.md ADDED
@@ -0,0 +1,498 @@
1
+ # Errors - Exception Handling and Custom Errors
2
+
3
+ **File**: `lib/pg_multitenant_schemas/errors.rb`
4
+
5
+ ## 📋 Overview
6
+
7
+ The `Errors` module defines custom exception classes for the PG Multitenant Schemas gem, providing specific error types for different failure scenarios and better error handling throughout the application.
8
+
9
+ ## 🎯 Purpose
10
+
11
+ - **Specific Exceptions**: Different error types for different failure scenarios
12
+ - **Better Debugging**: Clear error messages with context information
13
+ - **Error Handling**: Structured error handling throughout the gem
14
+ - **User Experience**: Meaningful error messages for developers
15
+
16
+ ## 🔧 Exception Classes
17
+
18
+ ### Core Exceptions
19
+
20
+ ```ruby
21
+ module PgMultitenantSchemas
22
+ # Base exception class for all gem-related errors
23
+ class Error < StandardError; end
24
+
25
+ # Configuration-related errors
26
+ class ConfigurationError < Error; end
27
+
28
+ # Schema operation failures
29
+ class SchemaError < Error; end
30
+
31
+ # Tenant resolution failures
32
+ class TenantNotFoundError < Error; end
33
+
34
+ # Migration-related errors
35
+ class MigrationError < Error; end
36
+
37
+ # Connection-related errors
38
+ class ConnectionError < Error; end
39
+ end
40
+ ```
41
+
42
+ ### Detailed Exception Definitions
43
+
44
+ #### Base Error Class
45
+
46
+ ```ruby
47
+ class Error < StandardError
48
+ attr_reader :context, :tenant, :schema
49
+
50
+ def initialize(message, context: nil, tenant: nil, schema: nil)
51
+ @context = context
52
+ @tenant = tenant
53
+ @schema = schema
54
+ super(message)
55
+ end
56
+
57
+ def to_h
58
+ {
59
+ error: self.class.name,
60
+ message: message,
61
+ context: context,
62
+ tenant: tenant&.to_s,
63
+ schema: schema
64
+ }
65
+ end
66
+ end
67
+ ```
68
+
69
+ #### Configuration Errors
70
+
71
+ ```ruby
72
+ class ConfigurationError < Error
73
+ def self.missing_tenant_model
74
+ new("Tenant model not configured. Set config.tenant_model in initializer.")
75
+ end
76
+
77
+ def self.invalid_connection_class(class_name)
78
+ new("Invalid connection class: #{class_name}. Class not found or not an ActiveRecord class.")
79
+ end
80
+
81
+ def self.missing_default_schema
82
+ new("Default schema not configured. Set config.default_schema in initializer.")
83
+ end
84
+ end
85
+ ```
86
+
87
+ #### Schema Errors
88
+
89
+ ```ruby
90
+ class SchemaError < Error
91
+ def self.schema_not_found(schema_name)
92
+ new("Schema '#{schema_name}' does not exist", schema: schema_name)
93
+ end
94
+
95
+ def self.schema_already_exists(schema_name)
96
+ new("Schema '#{schema_name}' already exists", schema: schema_name)
97
+ end
98
+
99
+ def self.invalid_schema_name(schema_name)
100
+ new("Invalid schema name: '#{schema_name}'. Schema names must be valid PostgreSQL identifiers.", schema: schema_name)
101
+ end
102
+
103
+ def self.cannot_drop_public_schema
104
+ new("Cannot drop the public schema", schema: 'public')
105
+ end
106
+
107
+ def self.schema_switch_failed(schema_name, original_error)
108
+ new("Failed to switch to schema '#{schema_name}': #{original_error.message}", schema: schema_name)
109
+ end
110
+ end
111
+ ```
112
+
113
+ #### Tenant Errors
114
+
115
+ ```ruby
116
+ class TenantNotFoundError < Error
117
+ def self.by_subdomain(subdomain)
118
+ new("Tenant not found with subdomain: #{subdomain}", context: { subdomain: subdomain })
119
+ end
120
+
121
+ def self.by_domain(domain)
122
+ new("Tenant not found with domain: #{domain}", context: { domain: domain })
123
+ end
124
+
125
+ def self.by_id(tenant_id)
126
+ new("Tenant not found with ID: #{tenant_id}", context: { tenant_id: tenant_id })
127
+ end
128
+
129
+ def self.from_request(request_info)
130
+ new("Could not resolve tenant from request", context: request_info)
131
+ end
132
+ end
133
+ ```
134
+
135
+ #### Migration Errors
136
+
137
+ ```ruby
138
+ class MigrationError < Error
139
+ def self.migration_failed(schema_name, migration_version, original_error)
140
+ new(
141
+ "Migration #{migration_version} failed for schema #{schema_name}: #{original_error.message}",
142
+ schema: schema_name,
143
+ context: { migration_version: migration_version, original_error: original_error.class.name }
144
+ )
145
+ end
146
+
147
+ def self.rollback_failed(schema_name, steps, original_error)
148
+ new(
149
+ "Failed to rollback #{steps} steps for schema #{schema_name}: #{original_error.message}",
150
+ schema: schema_name,
151
+ context: { rollback_steps: steps, original_error: original_error.class.name }
152
+ )
153
+ end
154
+
155
+ def self.no_pending_migrations(schema_name)
156
+ new("No pending migrations for schema: #{schema_name}", schema: schema_name)
157
+ end
158
+ end
159
+ ```
160
+
161
+ #### Connection Errors
162
+
163
+ ```ruby
164
+ class ConnectionError < Error
165
+ def self.connection_not_available
166
+ new("Database connection not available")
167
+ end
168
+
169
+ def self.invalid_connection_config(config)
170
+ new("Invalid connection configuration: #{config}")
171
+ end
172
+
173
+ def self.connection_timeout
174
+ new("Database connection timeout")
175
+ end
176
+ end
177
+ ```
178
+
179
+ ## 🔄 Usage Patterns
180
+
181
+ ### Error Handling in Components
182
+
183
+ #### Schema Switcher Error Handling
184
+
185
+ ```ruby
186
+ module PgMultitenantSchemas
187
+ class SchemaSwitcher
188
+ def self.switch_schema(schema_name)
189
+ validate_schema_name!(schema_name)
190
+
191
+ connection.execute("SET search_path TO #{schema_name}, public")
192
+ rescue PG::UndefinedObject => e
193
+ raise SchemaError.schema_not_found(schema_name)
194
+ rescue StandardError => e
195
+ raise SchemaError.schema_switch_failed(schema_name, e)
196
+ end
197
+
198
+ def self.create_schema(schema_name)
199
+ validate_schema_name!(schema_name)
200
+
201
+ connection.execute("CREATE SCHEMA IF NOT EXISTS #{schema_name}")
202
+ rescue PG::DuplicateSchema => e
203
+ raise SchemaError.schema_already_exists(schema_name)
204
+ rescue StandardError => e
205
+ raise SchemaError.new("Failed to create schema #{schema_name}: #{e.message}", schema: schema_name)
206
+ end
207
+
208
+ private
209
+
210
+ def self.validate_schema_name!(schema_name)
211
+ if schema_name.blank? || !schema_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
212
+ raise SchemaError.invalid_schema_name(schema_name)
213
+ end
214
+ end
215
+ end
216
+ end
217
+ ```
218
+
219
+ #### Tenant Resolver Error Handling
220
+
221
+ ```ruby
222
+ module PgMultitenantSchemas
223
+ class TenantResolver
224
+ def self.resolve_from_request(request)
225
+ subdomain = extract_subdomain(request)
226
+ return nil if subdomain.blank?
227
+
228
+ tenant = tenant_model.find_by(subdomain: subdomain)
229
+
230
+ unless tenant
231
+ raise TenantNotFoundError.by_subdomain(subdomain)
232
+ end
233
+
234
+ tenant
235
+ rescue ActiveRecord::ConnectionNotEstablished => e
236
+ raise ConnectionError.connection_not_available
237
+ rescue StandardError => e
238
+ request_info = {
239
+ host: request.host,
240
+ subdomain: subdomain,
241
+ user_agent: request.user_agent
242
+ }
243
+ raise TenantNotFoundError.from_request(request_info)
244
+ end
245
+ end
246
+ end
247
+ ```
248
+
249
+ #### Migrator Error Handling
250
+
251
+ ```ruby
252
+ module PgMultitenantSchemas
253
+ class Migrator
254
+ def self.migrate_tenant(schema_name)
255
+ puts "📦 Migrating schema: #{schema_name}"
256
+
257
+ Context.with_tenant(schema_name) do
258
+ ActiveRecord::Tasks::DatabaseTasks.migrate
259
+ end
260
+
261
+ puts " ✅ Completed migration for #{schema_name}"
262
+ rescue StandardError => e
263
+ puts " ❌ Error migrating #{schema_name}: #{e.message}"
264
+
265
+ # Re-raise as MigrationError with context
266
+ raise MigrationError.migration_failed(schema_name, 'latest', e)
267
+ end
268
+ end
269
+ end
270
+ ```
271
+
272
+ ### Application-Level Error Handling
273
+
274
+ #### Controller Error Handling
275
+
276
+ ```ruby
277
+ class ApplicationController < ActionController::Base
278
+ rescue_from PgMultitenantSchemas::TenantNotFoundError, with: :handle_tenant_not_found
279
+ rescue_from PgMultitenantSchemas::SchemaError, with: :handle_schema_error
280
+ rescue_from PgMultitenantSchemas::Error, with: :handle_multitenant_error
281
+
282
+ private
283
+
284
+ def handle_tenant_not_found(error)
285
+ Rails.logger.warn "Tenant not found: #{error.message}"
286
+
287
+ respond_to do |format|
288
+ format.html { redirect_to tenant_selection_path, alert: "Please select a valid tenant" }
289
+ format.json { render json: error.to_h, status: :not_found }
290
+ format.xml { render xml: error.to_h, status: :not_found }
291
+ end
292
+ end
293
+
294
+ def handle_schema_error(error)
295
+ Rails.logger.error "Schema error: #{error.message}"
296
+
297
+ respond_to do |format|
298
+ format.html { redirect_to root_path, alert: "A database error occurred" }
299
+ format.json { render json: { error: "Database error" }, status: :internal_server_error }
300
+ end
301
+ end
302
+
303
+ def handle_multitenant_error(error)
304
+ Rails.logger.error "Multitenant error: #{error.message}"
305
+ Rails.logger.error error.backtrace.join("\n")
306
+
307
+ respond_to do |format|
308
+ format.html { redirect_to root_path, alert: "An error occurred" }
309
+ format.json { render json: { error: "Internal error" }, status: :internal_server_error }
310
+ end
311
+ end
312
+ end
313
+ ```
314
+
315
+ #### API Error Handling
316
+
317
+ ```ruby
318
+ class Api::BaseController < ActionController::API
319
+ rescue_from PgMultitenantSchemas::TenantNotFoundError do |error|
320
+ render json: {
321
+ error: 'tenant_not_found',
322
+ message: error.message,
323
+ context: error.context
324
+ }, status: :not_found
325
+ end
326
+
327
+ rescue_from PgMultitenantSchemas::SchemaError do |error|
328
+ render json: {
329
+ error: 'schema_error',
330
+ message: error.message,
331
+ schema: error.schema
332
+ }, status: :unprocessable_entity
333
+ end
334
+
335
+ rescue_from PgMultitenantSchemas::MigrationError do |error|
336
+ render json: {
337
+ error: 'migration_error',
338
+ message: 'Database migration required',
339
+ schema: error.schema
340
+ }, status: :service_unavailable
341
+ end
342
+
343
+ rescue_from PgMultitenantSchemas::Error do |error|
344
+ render json: {
345
+ error: 'multitenant_error',
346
+ message: error.message
347
+ }, status: :internal_server_error
348
+ end
349
+ end
350
+ ```
351
+
352
+ #### Background Job Error Handling
353
+
354
+ ```ruby
355
+ class TenantJob < ApplicationJob
356
+ retry_on PgMultitenantSchemas::ConnectionError, wait: 30.seconds, attempts: 3
357
+ discard_on PgMultitenantSchemas::TenantNotFoundError
358
+
359
+ rescue_from PgMultitenantSchemas::SchemaError do |error|
360
+ Rails.logger.error "Schema error in job: #{error.message}"
361
+ # Notify administrators
362
+ AdminNotifier.schema_error(error).deliver_now
363
+ end
364
+
365
+ def perform(tenant_id, *args)
366
+ tenant = Tenant.find(tenant_id)
367
+
368
+ PgMultitenantSchemas::Context.with_tenant(tenant) do
369
+ process_tenant_work(*args)
370
+ end
371
+ rescue PgMultitenantSchemas::TenantNotFoundError => e
372
+ Rails.logger.error "Tenant #{tenant_id} not found for job"
373
+ # Don't retry for missing tenants
374
+ raise e
375
+ rescue PgMultitenantSchemas::MigrationError => e
376
+ Rails.logger.error "Migration required for tenant #{tenant_id}"
377
+ # Schedule migration and retry
378
+ TenantMigrationJob.perform_later(tenant_id)
379
+ raise e
380
+ end
381
+ end
382
+ ```
383
+
384
+ ## 🔍 Error Monitoring and Debugging
385
+
386
+ ### Error Logging Enhancement
387
+
388
+ ```ruby
389
+ module ErrorLogging
390
+ def self.included(base)
391
+ base.extend(ClassMethods)
392
+ end
393
+
394
+ module ClassMethods
395
+ def with_error_context(context = {})
396
+ yield
397
+ rescue PgMultitenantSchemas::Error => e
398
+ enhanced_error = e.class.new(
399
+ e.message,
400
+ context: e.context&.merge(context) || context,
401
+ tenant: e.tenant,
402
+ schema: e.schema
403
+ )
404
+
405
+ Rails.logger.error "#{e.class.name}: #{e.message}"
406
+ Rails.logger.error "Context: #{enhanced_error.context}"
407
+ Rails.logger.error "Tenant: #{enhanced_error.tenant}" if enhanced_error.tenant
408
+ Rails.logger.error "Schema: #{enhanced_error.schema}" if enhanced_error.schema
409
+
410
+ raise enhanced_error
411
+ end
412
+ end
413
+ end
414
+ ```
415
+
416
+ ### Error Reporting Integration
417
+
418
+ ```ruby
419
+ # For services like Sentry, Rollbar, etc.
420
+ module ErrorReporting
421
+ def self.report_multitenant_error(error, extra_context = {})
422
+ return unless error.is_a?(PgMultitenantSchemas::Error)
423
+
424
+ Sentry.capture_exception(error) do |scope|
425
+ scope.set_tag('component', 'pg_multitenant_schemas')
426
+ scope.set_tag('tenant', error.tenant) if error.tenant
427
+ scope.set_tag('schema', error.schema) if error.schema
428
+
429
+ scope.set_context('multitenant', {
430
+ context: error.context,
431
+ tenant: error.tenant,
432
+ schema: error.schema
433
+ }.merge(extra_context))
434
+ end
435
+ end
436
+ end
437
+
438
+ # Usage in error handlers
439
+ rescue PgMultitenantSchemas::Error => e
440
+ ErrorReporting.report_multitenant_error(e, { controller: self.class.name, action: action_name })
441
+ raise e
442
+ ```
443
+
444
+ ## 🚨 Error Prevention Best Practices
445
+
446
+ ### Validation and Guards
447
+
448
+ ```ruby
449
+ # Validate configuration on startup
450
+ PgMultitenantSchemas.configure do |config|
451
+ config.validate_on_startup = true
452
+ end
453
+
454
+ # Guard clauses in critical methods
455
+ def switch_to_tenant(tenant)
456
+ raise ArgumentError, "Tenant cannot be nil" if tenant.nil?
457
+ raise PgMultitenantSchemas::TenantNotFoundError.by_id(tenant.id) unless tenant.persisted?
458
+
459
+ # Proceed with tenant switching
460
+ end
461
+ ```
462
+
463
+ ### Error Recovery
464
+
465
+ ```ruby
466
+ # Automatic recovery for transient errors
467
+ module AutoRecovery
468
+ def with_retry(max_attempts: 3, backoff: 1.second)
469
+ attempts = 0
470
+
471
+ begin
472
+ yield
473
+ rescue PgMultitenantSchemas::ConnectionError => e
474
+ attempts += 1
475
+
476
+ if attempts < max_attempts
477
+ Rails.logger.warn "Connection error (attempt #{attempts}/#{max_attempts}): #{e.message}"
478
+ sleep(backoff * attempts)
479
+ retry
480
+ else
481
+ raise e
482
+ end
483
+ end
484
+ end
485
+ end
486
+ ```
487
+
488
+ ## 🔗 Related Components
489
+
490
+ - **[Configuration](configuration.md)**: Configuration validation and errors
491
+ - **[SchemaSwitcher](schema_switcher.md)**: Schema operation error handling
492
+ - **[Context](context.md)**: Context management error scenarios
493
+ - **[TenantResolver](tenant_resolver.md)**: Tenant resolution error handling
494
+ - **[Migrator](migrator.md)**: Migration error management
495
+
496
+ ## 📝 Examples
497
+
498
+ See [examples/error_handling.rb](../examples/) for comprehensive error handling patterns and recovery strategies.