ros-apartment 3.1.0 → 3.3.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/.gitignore +1 -0
- data/.rubocop.yml +142 -8
- data/.ruby-version +1 -1
- data/Appraisals +125 -30
- data/CLAUDE.md +210 -0
- data/CODE_OF_CONDUCT.md +71 -0
- data/Gemfile +15 -0
- data/README.md +49 -31
- data/Rakefile +44 -25
- data/docs/adapters.md +177 -0
- data/docs/architecture.md +274 -0
- data/docs/elevators.md +226 -0
- data/docs/images/log_example.png +0 -0
- data/lib/apartment/CLAUDE.md +300 -0
- data/lib/apartment/active_record/connection_handling.rb +2 -2
- data/lib/apartment/active_record/postgres/schema_dumper.rb +20 -0
- data/lib/apartment/active_record/postgresql_adapter.rb +19 -4
- data/lib/apartment/adapters/CLAUDE.md +314 -0
- data/lib/apartment/adapters/abstract_adapter.rb +24 -15
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
- data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
- data/lib/apartment/adapters/postgresql_adapter.rb +55 -36
- data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
- data/lib/apartment/console.rb +1 -1
- data/lib/apartment/custom_console.rb +7 -7
- data/lib/apartment/deprecation.rb +2 -5
- data/lib/apartment/elevators/CLAUDE.md +292 -0
- data/lib/apartment/elevators/domain.rb +1 -1
- data/lib/apartment/elevators/generic.rb +1 -1
- data/lib/apartment/elevators/host_hash.rb +3 -3
- data/lib/apartment/elevators/subdomain.rb +9 -5
- data/lib/apartment/log_subscriber.rb +1 -1
- data/lib/apartment/migrator.rb +17 -5
- data/lib/apartment/model.rb +1 -1
- data/lib/apartment/railtie.rb +3 -3
- data/lib/apartment/tasks/enhancements.rb +1 -1
- data/lib/apartment/tasks/task_helper.rb +6 -4
- data/lib/apartment/tenant.rb +3 -3
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +23 -11
- data/lib/generators/apartment/install/install_generator.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +2 -2
- data/lib/tasks/apartment.rake +25 -25
- data/ros-apartment.gemspec +10 -35
- metadata +44 -245
- data/.rubocop_todo.yml +0 -439
- /data/{CHANGELOG.md → legacy_CHANGELOG.md} +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# lib/apartment/ - Core Implementation Directory
|
|
2
|
+
|
|
3
|
+
This directory contains the core implementation of Apartment v3's multi-tenancy system.
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
lib/apartment/
|
|
9
|
+
├── adapters/ # Database-specific tenant isolation strategies
|
|
10
|
+
├── active_record/ # ActiveRecord patches and extensions
|
|
11
|
+
├── elevators/ # Rack middleware for automatic tenant switching
|
|
12
|
+
├── patches/ # Ruby/Rails core patches
|
|
13
|
+
├── tasks/ # Rake task utilities
|
|
14
|
+
├── console.rb # Rails console tenant switching utilities
|
|
15
|
+
├── custom_console.rb # Enhanced console with tenant prompts
|
|
16
|
+
├── deprecation.rb # Deprecation warnings configuration
|
|
17
|
+
├── log_subscriber.rb # ActiveSupport instrumentation for logging
|
|
18
|
+
├── migrator.rb # Tenant-specific migration runner
|
|
19
|
+
├── model.rb # ActiveRecord model extensions for excluded models
|
|
20
|
+
├── railtie.rb # Rails initialization and integration
|
|
21
|
+
├── tenant.rb # Public API facade for tenant operations
|
|
22
|
+
└── version.rb # Gem version constant
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Core Files
|
|
26
|
+
|
|
27
|
+
### tenant.rb - Public API Facade
|
|
28
|
+
|
|
29
|
+
**Purpose**: Main entry point for all tenant operations. Delegates to appropriate adapter.
|
|
30
|
+
|
|
31
|
+
**Key methods**:
|
|
32
|
+
- `create(tenant)` - Create new tenant
|
|
33
|
+
- `drop(tenant)` - Delete tenant
|
|
34
|
+
- `switch(tenant)` - Switch to tenant (block-based)
|
|
35
|
+
- `switch!(tenant)` - Immediate switch (no block)
|
|
36
|
+
- `current` - Get current tenant name
|
|
37
|
+
- `reset` - Return to default tenant
|
|
38
|
+
- `each` - Iterate over all tenants
|
|
39
|
+
|
|
40
|
+
**Adapter delegation pattern**: Uses `Forwardable` to delegate all operations to thread-local adapter instance. See delegation setup in `tenant.rb`.
|
|
41
|
+
|
|
42
|
+
**Thread-local storage**: Each thread maintains its own adapter via `Thread.current[:apartment_adapter]`. See `Apartment::Tenant.adapter` method for auto-detection logic.
|
|
43
|
+
|
|
44
|
+
### railtie.rb - Rails Integration
|
|
45
|
+
|
|
46
|
+
**Purpose**: Integrate Apartment with Rails initialization lifecycle.
|
|
47
|
+
|
|
48
|
+
**Responsibilities**:
|
|
49
|
+
1. **Configuration loading**: Load `config/initializers/apartment.rb`
|
|
50
|
+
2. **Adapter initialization**: Call `Apartment::Tenant.init` after Rails boot
|
|
51
|
+
3. **Console enhancement**: Add tenant switching helpers to Rails console
|
|
52
|
+
4. **Rake task loading**: Load Apartment rake tasks
|
|
53
|
+
5. **ActiveRecord instrumentation**: Set up logging subscriber
|
|
54
|
+
|
|
55
|
+
**Key integration points**: See Rails integration hooks in `railtie.rb` (`after_initialize`, `rake_tasks`, `console`).
|
|
56
|
+
|
|
57
|
+
**Excluded models initialization**: The railtie ensures excluded models establish separate connections after Rails boots but before the application serves requests. See excluded model setup in `railtie.rb`.
|
|
58
|
+
|
|
59
|
+
### console.rb / custom_console.rb - Interactive Debugging
|
|
60
|
+
|
|
61
|
+
**console.rb**: Basic console helpers
|
|
62
|
+
**custom_console.rb**: Enhanced prompt showing current tenant
|
|
63
|
+
|
|
64
|
+
**Features**:
|
|
65
|
+
- Display current tenant in prompt
|
|
66
|
+
- Quick switching helpers
|
|
67
|
+
- Tenant listing commands
|
|
68
|
+
|
|
69
|
+
**Implementation**: See `console.rb` and `custom_console.rb` for prompt customization and helper methods.
|
|
70
|
+
|
|
71
|
+
### migrator.rb - Tenant Migration Runner
|
|
72
|
+
|
|
73
|
+
**Purpose**: Run migrations across all tenants.
|
|
74
|
+
|
|
75
|
+
**Key functionality**:
|
|
76
|
+
- Detect pending migrations per tenant
|
|
77
|
+
- Run migrations in tenant context
|
|
78
|
+
- Handle migration failures gracefully
|
|
79
|
+
- Support parallel migration execution
|
|
80
|
+
|
|
81
|
+
**Integration**: Used by `rake apartment:migrate` task. See migration coordination logic in `migrator.rb` and task definitions in `tasks/enhancements.rake`.
|
|
82
|
+
|
|
83
|
+
**Parallel execution**: If `config.parallel_migration_threads > 0`, spawns threads to migrate multiple tenants concurrently. See parallel execution logic in `migrator.rb`.
|
|
84
|
+
|
|
85
|
+
### model.rb - Excluded Model Behavior
|
|
86
|
+
|
|
87
|
+
**Purpose**: Provide base module/behavior for excluded models.
|
|
88
|
+
|
|
89
|
+
**Functionality**:
|
|
90
|
+
- Establish separate connection to default database
|
|
91
|
+
- Bypass tenant switching
|
|
92
|
+
- Maintain global data across tenants
|
|
93
|
+
|
|
94
|
+
**Behavior**: When a model is in `Apartment.excluded_models`, it automatically establishes connection to default database and bypasses tenant switching. See connection handling in `model.rb` and `AbstractAdapter#process_excluded_models`.
|
|
95
|
+
|
|
96
|
+
### log_subscriber.rb - Instrumentation
|
|
97
|
+
|
|
98
|
+
**Purpose**: Subscribe to ActiveSupport notifications for logging tenant operations.
|
|
99
|
+
|
|
100
|
+
**Events logged**:
|
|
101
|
+
- Tenant creation
|
|
102
|
+
- Tenant switching
|
|
103
|
+
- Tenant deletion
|
|
104
|
+
- Migration execution
|
|
105
|
+
|
|
106
|
+
**Configuration**: Set `config.active_record_log = true` to enable. See event subscriptions in `log_subscriber.rb` and configuration options in `lib/apartment.rb`.
|
|
107
|
+
|
|
108
|
+
### version.rb - Version Management
|
|
109
|
+
|
|
110
|
+
**Purpose**: Define gem version constant. Used by gemspec and for version checking. See `version.rb`.
|
|
111
|
+
|
|
112
|
+
### deprecation.rb - Deprecation Warnings
|
|
113
|
+
|
|
114
|
+
**Purpose**: Configure ActiveSupport::Deprecation for Apartment.
|
|
115
|
+
|
|
116
|
+
**Implementation**: Sets up deprecation warnings targeting v4.0. See `deprecation.rb` for DEPRECATOR constant.
|
|
117
|
+
|
|
118
|
+
## Subdirectories
|
|
119
|
+
|
|
120
|
+
### adapters/
|
|
121
|
+
|
|
122
|
+
Database-specific implementations of tenant operations. See `lib/apartment/adapters/CLAUDE.md`.
|
|
123
|
+
|
|
124
|
+
**Key files**:
|
|
125
|
+
- `abstract_adapter.rb` - Base adapter with common logic
|
|
126
|
+
- `postgresql_adapter.rb` - PostgreSQL schema-based isolation
|
|
127
|
+
- `mysql2_adapter.rb` - MySQL database-based isolation
|
|
128
|
+
- `sqlite3_adapter.rb` - SQLite file-based isolation
|
|
129
|
+
|
|
130
|
+
### active_record/
|
|
131
|
+
|
|
132
|
+
ActiveRecord patches and extensions for tenant-aware behavior. See `lib/apartment/active_record/CLAUDE.md`.
|
|
133
|
+
|
|
134
|
+
**Key files**:
|
|
135
|
+
- `connection_handling.rb` - Patches to AR connection management
|
|
136
|
+
- `schema_migration.rb` - Tenant-aware schema_migrations table
|
|
137
|
+
- `postgresql_adapter.rb` - PostgreSQL-specific AR extensions
|
|
138
|
+
- `postgres/schema_dumper.rb` - Custom schema dumping (Rails 7.1+)
|
|
139
|
+
|
|
140
|
+
### elevators/
|
|
141
|
+
|
|
142
|
+
Rack middleware for automatic tenant detection. See `lib/apartment/elevators/CLAUDE.md`.
|
|
143
|
+
|
|
144
|
+
**Key files**:
|
|
145
|
+
- `generic.rb` - Base elevator with customizable logic
|
|
146
|
+
- `subdomain.rb` - Switch based on subdomain
|
|
147
|
+
- `domain.rb` - Switch based on domain
|
|
148
|
+
- `host.rb` - Switch based on full hostname
|
|
149
|
+
- `host_hash.rb` - Switch based on hostname→tenant mapping
|
|
150
|
+
|
|
151
|
+
### tasks/
|
|
152
|
+
|
|
153
|
+
Rake task utilities and enhancements.
|
|
154
|
+
|
|
155
|
+
**Key files**:
|
|
156
|
+
- `enhancements.rb` - Rake task definitions (migrate, seed, create, drop)
|
|
157
|
+
- `task_helper.rb` - Shared task utilities
|
|
158
|
+
|
|
159
|
+
## Data Flow
|
|
160
|
+
|
|
161
|
+
### Tenant Creation Flow
|
|
162
|
+
|
|
163
|
+
1. User calls `Apartment::Tenant.create('acme')`
|
|
164
|
+
2. Delegates to adapter which executes callbacks, creates schema/database, imports schema, optionally runs seeds
|
|
165
|
+
3. Returns to user code
|
|
166
|
+
|
|
167
|
+
**See**: `Apartment::Tenant.create` and `AbstractAdapter#create` for orchestration.
|
|
168
|
+
|
|
169
|
+
### Tenant Switching Flow
|
|
170
|
+
|
|
171
|
+
1. User calls `Apartment::Tenant.switch('acme') { ... }`
|
|
172
|
+
2. Adapter stores current tenant, switches connection, yields to block, ensures rollback in ensure clause
|
|
173
|
+
3. Returns to user code with tenant automatically restored
|
|
174
|
+
|
|
175
|
+
**See**: `AbstractAdapter#switch` method for implementation.
|
|
176
|
+
|
|
177
|
+
### Request Processing Flow (with Elevator)
|
|
178
|
+
|
|
179
|
+
1. HTTP Request arrives
|
|
180
|
+
2. Elevator extracts tenant, calls `Apartment::Tenant.switch`
|
|
181
|
+
3. Application processes in tenant context
|
|
182
|
+
4. Elevator ensures tenant reset
|
|
183
|
+
|
|
184
|
+
**See**: `elevators/generic.rb` for middleware pattern.
|
|
185
|
+
|
|
186
|
+
## Thread Safety
|
|
187
|
+
|
|
188
|
+
### Current Implementation (v3)
|
|
189
|
+
|
|
190
|
+
**Thread-local adapter storage**: Uses `Thread.current[:apartment_adapter]` for isolation.
|
|
191
|
+
|
|
192
|
+
**Implications**:
|
|
193
|
+
- ✅ Each thread has isolated tenant context
|
|
194
|
+
- ✅ Safe for multi-threaded servers (Puma)
|
|
195
|
+
- ✅ Safe for background jobs (Sidekiq)
|
|
196
|
+
- ❌ NOT fiber-safe (fibers share thread storage)
|
|
197
|
+
- ❌ Global mutable state within thread
|
|
198
|
+
|
|
199
|
+
**See**: `Apartment::Tenant.adapter` method for thread-local implementation.
|
|
200
|
+
|
|
201
|
+
## Configuration Integration
|
|
202
|
+
|
|
203
|
+
### Loading Process
|
|
204
|
+
|
|
205
|
+
1. Rails boots
|
|
206
|
+
2. `config/initializers/apartment.rb` loads
|
|
207
|
+
3. `Apartment.configure` executes
|
|
208
|
+
4. Configuration stored in module instance variables
|
|
209
|
+
5. `Railtie.after_initialize` fires
|
|
210
|
+
6. `Apartment::Tenant.init` called
|
|
211
|
+
7. Excluded models processed
|
|
212
|
+
8. Adapter initialized (lazy, on first use)
|
|
213
|
+
|
|
214
|
+
**See**: Configuration methods in `lib/apartment.rb` and initialization hooks in `railtie.rb`.
|
|
215
|
+
|
|
216
|
+
### Configuration Access
|
|
217
|
+
|
|
218
|
+
Available configuration methods: `Apartment.tenant_names`, `Apartment.excluded_models`, `Apartment.connection_class`, `Apartment.db_migrate_tenants`. See `lib/apartment.rb` for all configuration options.
|
|
219
|
+
|
|
220
|
+
## Error Handling
|
|
221
|
+
|
|
222
|
+
### Exception Hierarchy
|
|
223
|
+
|
|
224
|
+
- `Apartment::ApartmentError` - Base exception for all Apartment errors
|
|
225
|
+
- `Apartment::TenantNotFound` - Raised when switching to nonexistent tenant
|
|
226
|
+
- `Apartment::TenantExists` - Raised when creating duplicate tenant
|
|
227
|
+
|
|
228
|
+
**See**: Adapter `connect_to_new` methods raise `TenantNotFound`. See `AbstractAdapter#switch` for error handling.
|
|
229
|
+
|
|
230
|
+
### Automatic Cleanup
|
|
231
|
+
|
|
232
|
+
The `switch` method guarantees cleanup via ensure block, falling back to default tenant if rollback fails. See `AbstractAdapter#switch` for implementation.
|
|
233
|
+
|
|
234
|
+
## Extending Apartment
|
|
235
|
+
|
|
236
|
+
### Adding Custom Adapter
|
|
237
|
+
|
|
238
|
+
1. Create file: `lib/apartment/adapters/custom_adapter.rb`
|
|
239
|
+
2. Subclass `AbstractAdapter`
|
|
240
|
+
3. Implement required methods
|
|
241
|
+
4. Add factory method to `tenant.rb`
|
|
242
|
+
|
|
243
|
+
See `docs/adapters.md` for details.
|
|
244
|
+
|
|
245
|
+
### Adding Custom Elevator
|
|
246
|
+
|
|
247
|
+
1. Create file: `app/middleware/custom_elevator.rb`
|
|
248
|
+
2. Subclass `Apartment::Elevators::Generic`
|
|
249
|
+
3. Override `parse_tenant_name(request)`
|
|
250
|
+
4. Add to middleware stack in `config/application.rb`
|
|
251
|
+
|
|
252
|
+
See `docs/elevators.md` for details.
|
|
253
|
+
|
|
254
|
+
### Adding Custom Callbacks
|
|
255
|
+
|
|
256
|
+
Use ActiveSupport::Callbacks to hook into `:create` and `:switch` events. See callback definitions in `AbstractAdapter` and README.md for configuration examples.
|
|
257
|
+
|
|
258
|
+
## Testing Considerations
|
|
259
|
+
|
|
260
|
+
### RSpec Integration
|
|
261
|
+
|
|
262
|
+
Always reset tenant context in before/after hooks to prevent test isolation issues. See `spec/support/` for helper modules and `spec/spec_helper.rb` for configuration patterns.
|
|
263
|
+
|
|
264
|
+
### Creating Test Tenants
|
|
265
|
+
|
|
266
|
+
Create helpers for tenant lifecycle management to avoid duplication. See `spec/support/apartment_helper.rb` for patterns.
|
|
267
|
+
|
|
268
|
+
## Debugging Tips
|
|
269
|
+
|
|
270
|
+
### Enable Verbose Logging
|
|
271
|
+
|
|
272
|
+
Set `config.active_record_log = true` in initializer. See logging configuration in `lib/apartment.rb`.
|
|
273
|
+
|
|
274
|
+
### Check Current Tenant
|
|
275
|
+
|
|
276
|
+
Use `Apartment::Tenant.current` to inspect current tenant context.
|
|
277
|
+
|
|
278
|
+
### Inspect Adapter
|
|
279
|
+
|
|
280
|
+
Access `Apartment::Tenant.adapter` to inspect adapter class and configuration.
|
|
281
|
+
|
|
282
|
+
### Verify Excluded Models
|
|
283
|
+
|
|
284
|
+
Iterate `Apartment.excluded_models` and check each model's connection configuration.
|
|
285
|
+
|
|
286
|
+
## Common Pitfalls
|
|
287
|
+
|
|
288
|
+
1. **Not using block-based switching**: Always use `switch` with block, not `switch!`
|
|
289
|
+
2. **Elevator positioning**: Must be before session/auth middleware
|
|
290
|
+
3. **Excluded model relationships**: Use `has_many :through`, not `has_and_belongs_to_many`
|
|
291
|
+
4. **Thread safety assumptions**: Remember adapters are thread-local, not global
|
|
292
|
+
5. **Forgetting to reset**: In tests, always reset tenant in teardown
|
|
293
|
+
|
|
294
|
+
## References
|
|
295
|
+
|
|
296
|
+
- Main README: `/README.md`
|
|
297
|
+
- Architecture docs: `/docs/architecture.md`
|
|
298
|
+
- Adapter docs: `/docs/adapters.md`
|
|
299
|
+
- Elevator docs: `/docs/elevators.md`
|
|
300
|
+
- ActiveRecord connection handling: Rails guides
|
|
@@ -15,10 +15,10 @@ module ActiveRecord # :nodoc:
|
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
else
|
|
18
|
-
def connected_to_with_tenant(role: nil, prevent_writes: false, &blk)
|
|
18
|
+
def connected_to_with_tenant(role: nil, shard: nil, prevent_writes: false, &blk)
|
|
19
19
|
current_tenant = Apartment::Tenant.current
|
|
20
20
|
|
|
21
|
-
connected_to_without_tenant(role: role, prevent_writes: prevent_writes) do
|
|
21
|
+
connected_to_without_tenant(role: role, shard: shard, prevent_writes: prevent_writes) do
|
|
22
22
|
Apartment::Tenant.switch!(current_tenant)
|
|
23
23
|
yield(blk)
|
|
24
24
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This patch prevents `create_schema` from being added to db/schema.rb as schemas are managed by Apartment
|
|
4
|
+
# not ActiveRecord like they would be in a vanilla Rails setup.
|
|
5
|
+
|
|
6
|
+
require 'active_record/connection_adapters/abstract/schema_dumper'
|
|
7
|
+
require 'active_record/connection_adapters/postgresql/schema_dumper'
|
|
8
|
+
|
|
9
|
+
module ActiveRecord
|
|
10
|
+
module ConnectionAdapters
|
|
11
|
+
module PostgreSQL
|
|
12
|
+
class SchemaDumper
|
|
13
|
+
alias _original_schemas schemas
|
|
14
|
+
def schemas(stream)
|
|
15
|
+
_original_schemas(stream) unless Apartment.use_schemas
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -12,23 +12,38 @@ module Apartment::PostgreSqlAdapterPatch
|
|
|
12
12
|
# for JDBC driver, if rescued in super_method, trim leading and trailing quotes
|
|
13
13
|
res.delete!('"') if defined?(JRUBY_VERSION)
|
|
14
14
|
|
|
15
|
-
schema_prefix = "#{
|
|
16
|
-
default_tenant_prefix = "#{Apartment::Tenant.default_tenant}."
|
|
15
|
+
schema_prefix = "#{sequence_schema(res)}."
|
|
17
16
|
|
|
18
17
|
# NOTE: Excluded models should always access the sequence from the default
|
|
19
18
|
# tenant schema
|
|
20
19
|
if excluded_model?(table)
|
|
21
|
-
|
|
20
|
+
default_tenant_prefix = "#{Apartment::Tenant.default_tenant}."
|
|
21
|
+
|
|
22
|
+
# Unless the res is already prefixed with the default_tenant_prefix
|
|
23
|
+
# we should delete the schema_prefix and add the default_tenant_prefix
|
|
24
|
+
unless res&.starts_with?(default_tenant_prefix)
|
|
25
|
+
res&.delete_prefix!(schema_prefix)
|
|
26
|
+
res = default_tenant_prefix + res
|
|
27
|
+
end
|
|
28
|
+
|
|
22
29
|
return res
|
|
23
30
|
end
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
# Delete the schema_prefix from the res if it is present
|
|
33
|
+
res&.delete_prefix!(schema_prefix)
|
|
26
34
|
|
|
27
35
|
res
|
|
28
36
|
end
|
|
29
37
|
|
|
30
38
|
private
|
|
31
39
|
|
|
40
|
+
def sequence_schema(sequence_name)
|
|
41
|
+
current = Apartment::Tenant.current
|
|
42
|
+
return current unless current.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
current.find { |schema| sequence_name.starts_with?("#{schema}.") }
|
|
45
|
+
end
|
|
46
|
+
|
|
32
47
|
def excluded_model?(table)
|
|
33
48
|
Apartment.excluded_models.any? { |m| m.constantize.table_name == table }
|
|
34
49
|
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# lib/apartment/adapters/ - Database Adapter Implementations
|
|
2
|
+
|
|
3
|
+
This directory contains database-specific implementations of tenant isolation strategies.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Adapters translate abstract tenant operations (create, switch, drop) into database-specific SQL commands and connection management.
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
adapters/
|
|
13
|
+
├── abstract_adapter.rb # Base class with shared logic
|
|
14
|
+
├── postgresql_adapter.rb # PostgreSQL schema-based isolation
|
|
15
|
+
├── postgis_adapter.rb # PostgreSQL with PostGIS extensions
|
|
16
|
+
├── mysql2_adapter.rb # MySQL database-based isolation (mysql2 gem)
|
|
17
|
+
├── trilogy_adapter.rb # MySQL database-based isolation (trilogy gem)
|
|
18
|
+
├── sqlite3_adapter.rb # SQLite file-based isolation
|
|
19
|
+
├── abstract_jdbc_adapter.rb # Base for JDBC adapters (JRuby)
|
|
20
|
+
├── jdbc_postgresql_adapter.rb # JDBC PostgreSQL adapter
|
|
21
|
+
└── jdbc_mysql_adapter.rb # JDBC MySQL adapter
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Adapter Hierarchy
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
AbstractAdapter
|
|
28
|
+
├── PostgresqlAdapter
|
|
29
|
+
│ ├── PostgisAdapter (PostgreSQL + spatial extensions)
|
|
30
|
+
│ └── JdbcPostgresqlAdapter (JDBC for JRuby)
|
|
31
|
+
├── Mysql2Adapter
|
|
32
|
+
│ ├── TrilogyAdapter (alternative MySQL driver)
|
|
33
|
+
│ └── JdbcMysqlAdapter (JDBC for JRuby)
|
|
34
|
+
└── Sqlite3Adapter
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## AbstractAdapter - Base Implementation
|
|
38
|
+
|
|
39
|
+
**Location**: `abstract_adapter.rb`
|
|
40
|
+
|
|
41
|
+
### Responsibilities
|
|
42
|
+
|
|
43
|
+
1. **Common tenant lifecycle logic**:
|
|
44
|
+
- Callback execution (`:create`, `:switch`)
|
|
45
|
+
- Schema import coordination
|
|
46
|
+
- Seed data execution
|
|
47
|
+
- Exception handling
|
|
48
|
+
|
|
49
|
+
2. **Excluded model management**:
|
|
50
|
+
- Establish separate connections for excluded models
|
|
51
|
+
- Ensure they bypass tenant switching
|
|
52
|
+
|
|
53
|
+
3. **Helper methods**:
|
|
54
|
+
- `environmentify(tenant)` - Add Rails env to tenant name
|
|
55
|
+
- `seed_data` - Load seeds.rb in tenant context
|
|
56
|
+
- `each(tenants)` - Iterate over tenants
|
|
57
|
+
|
|
58
|
+
### Abstract Methods (Subclasses Must Implement)
|
|
59
|
+
|
|
60
|
+
- `create_tenant(tenant)` - Create the tenant (schema/database/file)
|
|
61
|
+
- `connect_to_new(tenant)` - Switch to tenant (change connection or search_path)
|
|
62
|
+
- `drop_command(conn, tenant)` - Drop the tenant
|
|
63
|
+
- `current` - Get current tenant name
|
|
64
|
+
|
|
65
|
+
**See**: Abstract method definitions in `abstract_adapter.rb`.
|
|
66
|
+
|
|
67
|
+
### Common Logic Provided
|
|
68
|
+
|
|
69
|
+
**Tenant creation**: Runs callbacks, creates tenant via subclass, switches context, imports schema, optionally seeds data. See `AbstractAdapter#create` method.
|
|
70
|
+
|
|
71
|
+
**Tenant switching**: Stores previous tenant, switches, yields to block, ensures rollback in ensure clause with fallback to default. See `AbstractAdapter#switch` method.
|
|
72
|
+
|
|
73
|
+
**Schema import**: Loads `db/schema.rb` or custom schema file. See schema import logic in `abstract_adapter.rb`.
|
|
74
|
+
|
|
75
|
+
### Helper Methods
|
|
76
|
+
|
|
77
|
+
**Environmentify**: Adds Rails environment prefix/suffix to tenant name based on configuration. See `AbstractAdapter#environmentify` method.
|
|
78
|
+
|
|
79
|
+
**Excluded model processing**: Establishes separate connections for excluded models. See `AbstractAdapter#process_excluded_models` method.
|
|
80
|
+
|
|
81
|
+
## PostgreSQL Adapter
|
|
82
|
+
|
|
83
|
+
**Location**: `postgresql_adapter.rb`
|
|
84
|
+
|
|
85
|
+
### Strategy
|
|
86
|
+
|
|
87
|
+
Uses **PostgreSQL schemas** (namespaces) for tenant isolation.
|
|
88
|
+
|
|
89
|
+
### Key Implementation Details
|
|
90
|
+
|
|
91
|
+
**Create tenant**: Executes `CREATE SCHEMA` SQL command. See `PostgresqlAdapter#create_tenant` method.
|
|
92
|
+
|
|
93
|
+
**Switch tenant**: Changes `search_path` to target schema. See `PostgresqlAdapter#connect_to_new` method.
|
|
94
|
+
|
|
95
|
+
**Drop tenant**: Executes `DROP SCHEMA CASCADE`. See `PostgresqlAdapter#drop_command` method.
|
|
96
|
+
|
|
97
|
+
**Get current tenant**: Returns instance variable tracking current schema. See `PostgresqlAdapter#current` method.
|
|
98
|
+
|
|
99
|
+
### Search Path Mechanics
|
|
100
|
+
|
|
101
|
+
PostgreSQL searches schemas in order defined by `search_path`. Queries resolve to first matching table. Search path includes tenant schema, persistent schemas, then public. See search path construction in `PostgresqlAdapter#connect_to_new`.
|
|
102
|
+
|
|
103
|
+
### Persistent Schemas
|
|
104
|
+
|
|
105
|
+
Configured via `config.persistent_schemas` to specify schemas that remain in search path across all tenants.
|
|
106
|
+
|
|
107
|
+
**Use cases**:
|
|
108
|
+
- Shared PostgreSQL extensions (uuid-ossp, hstore, postgis)
|
|
109
|
+
- Utility functions/views shared across tenants
|
|
110
|
+
- Reference data tables
|
|
111
|
+
|
|
112
|
+
**See**: README.md for configuration examples.
|
|
113
|
+
|
|
114
|
+
### Excluded Names (pg_excluded_names)
|
|
115
|
+
|
|
116
|
+
Configured via `config.pg_excluded_names` to exclude tables/schemas from tenant cloning.
|
|
117
|
+
|
|
118
|
+
**Use cases**:
|
|
119
|
+
- Temporary tables
|
|
120
|
+
- Backup tables
|
|
121
|
+
- Staging/import tables
|
|
122
|
+
|
|
123
|
+
**See**: README.md for configuration patterns.
|
|
124
|
+
|
|
125
|
+
### Performance Characteristics
|
|
126
|
+
|
|
127
|
+
- **Switching**: <1ms (SQL command)
|
|
128
|
+
- **Memory**: ~50MB total (shared connection pool)
|
|
129
|
+
- **Scalability**: 100+ tenants easily
|
|
130
|
+
- **Isolation**: Schema-level (good, not absolute)
|
|
131
|
+
|
|
132
|
+
## PostGIS Adapter
|
|
133
|
+
|
|
134
|
+
**Location**: `postgis_adapter.rb`
|
|
135
|
+
|
|
136
|
+
### Strategy
|
|
137
|
+
|
|
138
|
+
Extends `PostgresqlAdapter` with PostGIS spatial extension support.
|
|
139
|
+
|
|
140
|
+
### Key Differences
|
|
141
|
+
|
|
142
|
+
**Tenant creation**: Extends base PostgresqlAdapter to automatically enable PostGIS extensions in new schemas. See `PostgisAdapter#create_tenant` method.
|
|
143
|
+
|
|
144
|
+
**Schema dumping**: Custom logic to handle spatial types and indexes correctly. See `active_record/postgres/schema_dumper.rb`.
|
|
145
|
+
|
|
146
|
+
### Configuration
|
|
147
|
+
|
|
148
|
+
Typically includes PostGIS-related schemas in `persistent_schemas`. See README.md for configuration.
|
|
149
|
+
|
|
150
|
+
## MySQL Adapters
|
|
151
|
+
|
|
152
|
+
**Locations**: `mysql2_adapter.rb`, `trilogy_adapter.rb`
|
|
153
|
+
|
|
154
|
+
### Strategy
|
|
155
|
+
|
|
156
|
+
Uses **separate databases** for each tenant.
|
|
157
|
+
|
|
158
|
+
### Key Implementation Details
|
|
159
|
+
|
|
160
|
+
**Create tenant**: Executes `CREATE DATABASE` SQL command. See `Mysql2Adapter#create_tenant` method.
|
|
161
|
+
|
|
162
|
+
**Switch tenant**: Establishes new connection with different database name. See `Mysql2Adapter#connect_to_new` method.
|
|
163
|
+
|
|
164
|
+
**Drop tenant**: Executes `DROP DATABASE`. See `Mysql2Adapter#drop_command` method.
|
|
165
|
+
|
|
166
|
+
**Get current database**: Queries current database name from connection. See `Mysql2Adapter#current` method.
|
|
167
|
+
|
|
168
|
+
### Connection Management
|
|
169
|
+
|
|
170
|
+
Each tenant switch establishes new connection to different database. This creates connection pool overhead compared to PostgreSQL schemas. See `Mysql2Adapter#connect_to_new` for connection establishment.
|
|
171
|
+
|
|
172
|
+
### Multi-Server Support
|
|
173
|
+
|
|
174
|
+
MySQL adapters support hash-based configuration mapping tenant names to full connection configs, enabling different tenants on different servers. See README.md for configuration examples.
|
|
175
|
+
|
|
176
|
+
### Performance Characteristics
|
|
177
|
+
|
|
178
|
+
- **Switching**: 10-50ms (connection establishment)
|
|
179
|
+
- **Memory**: ~20MB per active tenant (connection pool)
|
|
180
|
+
- **Scalability**: 10-50 concurrent tenants
|
|
181
|
+
- **Isolation**: Database-level (excellent)
|
|
182
|
+
|
|
183
|
+
### Trilogy Adapter
|
|
184
|
+
|
|
185
|
+
**Location**: `trilogy_adapter.rb`
|
|
186
|
+
|
|
187
|
+
Identical to `Mysql2Adapter` but uses the `trilogy` gem (modern MySQL client).
|
|
188
|
+
|
|
189
|
+
**Usage**: Auto-selected if `adapter: trilogy` in `database.yml`.
|
|
190
|
+
|
|
191
|
+
## SQLite Adapter
|
|
192
|
+
|
|
193
|
+
**Location**: `sqlite3_adapter.rb`
|
|
194
|
+
|
|
195
|
+
### Strategy
|
|
196
|
+
|
|
197
|
+
Uses **separate database files** for each tenant.
|
|
198
|
+
|
|
199
|
+
### Key Implementation Details
|
|
200
|
+
|
|
201
|
+
**Create tenant**: Creates new SQLite file and establishes connection. See `Sqlite3Adapter#create_tenant` method.
|
|
202
|
+
|
|
203
|
+
**Switch tenant**: Establishes connection to different database file. See `Sqlite3Adapter#connect_to_new` method.
|
|
204
|
+
|
|
205
|
+
**Drop tenant**: Deletes database file. See `Sqlite3Adapter#drop_command` method.
|
|
206
|
+
|
|
207
|
+
**Database file path**: Constructs file path in db/ directory. See file path construction in `Sqlite3Adapter`.
|
|
208
|
+
|
|
209
|
+
### Use Cases
|
|
210
|
+
|
|
211
|
+
- ✅ **Testing**: Each test tenant is isolated file
|
|
212
|
+
- ✅ **Development**: Easy to inspect individual tenant data
|
|
213
|
+
- ✅ **Single-user apps**: Desktop or embedded applications
|
|
214
|
+
- ❌ **Production**: Not suitable for concurrent multi-user access
|
|
215
|
+
|
|
216
|
+
### Performance Characteristics
|
|
217
|
+
|
|
218
|
+
- **Switching**: 5-20ms (file I/O + connection)
|
|
219
|
+
- **Memory**: ~5MB per database file
|
|
220
|
+
- **Scalability**: Not recommended for production multi-tenant
|
|
221
|
+
- **Isolation**: Complete (separate files)
|
|
222
|
+
|
|
223
|
+
## JDBC Adapters (JRuby)
|
|
224
|
+
|
|
225
|
+
**Locations**: `abstract_jdbc_adapter.rb`, `jdbc_postgresql_adapter.rb`, `jdbc_mysql_adapter.rb`
|
|
226
|
+
|
|
227
|
+
### Purpose
|
|
228
|
+
|
|
229
|
+
Support JRuby deployments using JDBC drivers.
|
|
230
|
+
|
|
231
|
+
### Implementation
|
|
232
|
+
|
|
233
|
+
Inherit from standard adapters but use JDBC-specific connection handling. See `jdbc_postgresql_adapter.rb` and `jdbc_mysql_adapter.rb`.
|
|
234
|
+
|
|
235
|
+
### Auto-Detection
|
|
236
|
+
|
|
237
|
+
JRuby detection happens in `tenant.rb` - automatically selects JDBC adapters when running on JRuby. See adapter factory logic in `Apartment::Tenant.adapter_method`.
|
|
238
|
+
|
|
239
|
+
## Adapter Selection Matrix
|
|
240
|
+
|
|
241
|
+
| Adapter | Database Type | Strategy | Speed | Scalability | Isolation | Best For |
|
|
242
|
+
|------------------------|---------------|--------------|--------------|-------------|-----------|-------------------------|
|
|
243
|
+
| PostgresqlAdapter | PostgreSQL | Schemas | Very Fast | Excellent | Good | 100+ tenants |
|
|
244
|
+
| PostgisAdapter | PostGIS | Schemas | Very Fast | Excellent | Good | Spatial data apps |
|
|
245
|
+
| Mysql2Adapter | MySQL | Databases | Moderate | Good | Excellent | Complete isolation |
|
|
246
|
+
| TrilogyAdapter | MySQL | Databases | Moderate | Good | Excellent | Modern MySQL client |
|
|
247
|
+
| Sqlite3Adapter | SQLite | Files | Moderate | Poor | Excellent | Testing, development |
|
|
248
|
+
| JdbcPostgresqlAdapter | PostgreSQL | Schemas | Very Fast | Excellent | Good | JRuby deployments |
|
|
249
|
+
| JdbcMysqlAdapter | MySQL | Databases | Moderate | Good | Excellent | JRuby deployments |
|
|
250
|
+
|
|
251
|
+
## Creating Custom Adapters
|
|
252
|
+
|
|
253
|
+
To support new databases: subclass `AbstractAdapter`, implement required methods (`create_tenant`, `connect_to_new`, `drop_command`, `current`), register factory method in `tenant.rb`, and configure in `database.yml`.
|
|
254
|
+
|
|
255
|
+
**See**: Existing adapters for patterns (`postgresql_adapter.rb` is most complex, `sqlite3_adapter.rb` is simplest), and `docs/adapters.md` for design rationale.
|
|
256
|
+
|
|
257
|
+
## Testing Adapters
|
|
258
|
+
|
|
259
|
+
### Adapter-Specific Tests
|
|
260
|
+
|
|
261
|
+
Each adapter has comprehensive specs covering tenant creation, switching, deletion, error handling, and callbacks. See `spec/adapters/` for test patterns.
|
|
262
|
+
|
|
263
|
+
## Debugging Adapters
|
|
264
|
+
|
|
265
|
+
### Check Current Adapter
|
|
266
|
+
|
|
267
|
+
Use `Apartment::Tenant.adapter.class.name` to inspect adapter type.
|
|
268
|
+
|
|
269
|
+
### Inspect Configuration
|
|
270
|
+
|
|
271
|
+
Access `adapter.instance_variable_get(:@config)` for configuration and `adapter.default_tenant` for default.
|
|
272
|
+
|
|
273
|
+
### Database-Specific Debugging
|
|
274
|
+
|
|
275
|
+
**PostgreSQL**: Execute `SHOW search_path` to verify current schema search path.
|
|
276
|
+
|
|
277
|
+
**MySQL**: Execute `SELECT DATABASE()` to verify current database name.
|
|
278
|
+
|
|
279
|
+
## Common Issues
|
|
280
|
+
|
|
281
|
+
### Issue: Schema/Database Not Created
|
|
282
|
+
|
|
283
|
+
**Cause**: Permissions, invalid names, or database errors
|
|
284
|
+
|
|
285
|
+
**Debug**: Wrap `Apartment::Tenant.create` in rescue block and inspect exception class and message.
|
|
286
|
+
|
|
287
|
+
### Issue: Switching Fails
|
|
288
|
+
|
|
289
|
+
**Cause**: Tenant doesn't exist or connection issues
|
|
290
|
+
|
|
291
|
+
**Debug**: Verify tenant in `Apartment.tenant_names` and check `adapter.current` state.
|
|
292
|
+
|
|
293
|
+
### Issue: Wrong Data After Switch
|
|
294
|
+
|
|
295
|
+
**Cause**: Improper cleanup or middleware ordering
|
|
296
|
+
|
|
297
|
+
**Solution**: Always use block-based switching, verify middleware order.
|
|
298
|
+
|
|
299
|
+
## Performance Optimization
|
|
300
|
+
|
|
301
|
+
### PostgreSQL: Connection Pooling
|
|
302
|
+
|
|
303
|
+
PostgreSQL adapters use shared connection pool across all tenants. Configure pool size in `database.yml`. See Rails connection pooling guides.
|
|
304
|
+
|
|
305
|
+
### MySQL: Connection Pool Caching
|
|
306
|
+
|
|
307
|
+
Consider implementing LRU cache for connection pools to limit memory usage with many tenants. Not implemented in v3 but possible via custom adapter.
|
|
308
|
+
|
|
309
|
+
## References
|
|
310
|
+
|
|
311
|
+
- PostgreSQL schemas: https://www.postgresql.org/docs/current/ddl-schemas.html
|
|
312
|
+
- MySQL databases: https://dev.mysql.com/doc/refman/8.0/en/creating-database.html
|
|
313
|
+
- ActiveRecord adapters: Rails source code
|
|
314
|
+
- AbstractAdapter source: `abstract_adapter.rb`
|