ros-apartment 3.2.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +93 -2
  4. data/.ruby-version +1 -1
  5. data/Appraisals +15 -0
  6. data/CLAUDE.md +210 -0
  7. data/README.md +1 -1
  8. data/Rakefile +6 -5
  9. data/docs/adapters.md +177 -0
  10. data/docs/architecture.md +274 -0
  11. data/docs/elevators.md +226 -0
  12. data/docs/images/log_example.png +0 -0
  13. data/lib/apartment/CLAUDE.md +300 -0
  14. data/lib/apartment/adapters/CLAUDE.md +314 -0
  15. data/lib/apartment/adapters/abstract_adapter.rb +24 -15
  16. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
  17. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
  18. data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
  19. data/lib/apartment/adapters/postgresql_adapter.rb +42 -19
  20. data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
  21. data/lib/apartment/console.rb +1 -1
  22. data/lib/apartment/custom_console.rb +7 -7
  23. data/lib/apartment/elevators/CLAUDE.md +292 -0
  24. data/lib/apartment/elevators/domain.rb +1 -1
  25. data/lib/apartment/elevators/generic.rb +1 -1
  26. data/lib/apartment/elevators/host_hash.rb +3 -3
  27. data/lib/apartment/elevators/subdomain.rb +9 -5
  28. data/lib/apartment/log_subscriber.rb +1 -1
  29. data/lib/apartment/migrator.rb +2 -2
  30. data/lib/apartment/model.rb +1 -1
  31. data/lib/apartment/railtie.rb +3 -3
  32. data/lib/apartment/tasks/enhancements.rb +1 -1
  33. data/lib/apartment/tasks/task_helper.rb +4 -4
  34. data/lib/apartment/tenant.rb +3 -3
  35. data/lib/apartment/version.rb +1 -1
  36. data/lib/apartment.rb +15 -9
  37. data/lib/generators/apartment/install/install_generator.rb +1 -1
  38. data/lib/generators/apartment/install/templates/apartment.rb +2 -2
  39. data/lib/tasks/apartment.rake +25 -25
  40. data/ros-apartment.gemspec +3 -3
  41. metadata +22 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7acbb6c519fef676bdf05370eb1de788bf262a30f83040fcb2455b3a950ad58
4
- data.tar.gz: c078d989df4e11e7d519bcb0c30c0a99cd8f692adc995407a6a7fdecbd5d6e2a
3
+ metadata.gz: 22097d27933cc7eee7bf0c5e3ad5cf1b84d9243dcfc04be00d6db07a91c9857b
4
+ data.tar.gz: 28e20f76b32b532db8db681503753216ca2847c66ab80e902c200ea43a82b05d
5
5
  SHA512:
6
- metadata.gz: 9ef24485b251bf0d68f1e2af3a6cb82312b25f457383ed4bff1eadf50ee2cacba8af21330cd03853186ee117b07e3454f9224485d81079c687b69fadc9fabbea
7
- data.tar.gz: 64c3423beb74472af3f84ad21152eed840162a6bf7d567b360f0a7216314545f7e1ca5eb5af9fb20b753fd681a4d40582478da6335ab69cc8e8795812362d74a
6
+ metadata.gz: 5f007c6b72251b371b02380628d53b5cf1c7b62e5620fb659b596e9dec1a62e7a2fc0bf0ad1f5253d99b852f31091ddcedf073ed5e64457c5430c58c79c8aa83
7
+ data.tar.gz: 53ccf42974cebbcc6f874f9a631fe366a3952ff36d993127173df34563848359f409a3beeaa37596896a75e8c85cccd63a5ce4a7ec70b7d3278bf7e3b8c7e394
data/.gitignore CHANGED
@@ -13,3 +13,4 @@ cookbooks
13
13
  tmp
14
14
  spec/dummy/db/*.sqlite3
15
15
  .DS_Store
16
+ .claude/
data/.rubocop.yml CHANGED
@@ -7,7 +7,7 @@ AllCops:
7
7
  - spec/dummy_engine/dummy_engine.gemspec
8
8
  - spec/schemas/**/*.rb
9
9
 
10
- require:
10
+ plugins:
11
11
  - rubocop-rails
12
12
  - rubocop-performance
13
13
  - rubocop-thread_safety
@@ -22,6 +22,26 @@ Layout/MultilineMethodCallIndentation:
22
22
  EnforcedStyle: indented
23
23
 
24
24
  Metrics/BlockLength:
25
+ Max: 30
26
+ Exclude:
27
+ - spec/**/*.rb
28
+ - lib/tasks/**/*.rake
29
+ - Rakefile
30
+
31
+ Metrics/MethodLength:
32
+ Max: 15
33
+ Exclude:
34
+ - spec/**/*.rb
35
+ - lib/apartment/tenant.rb
36
+
37
+ Metrics/AbcSize:
38
+ Max: 20
39
+ Exclude:
40
+ - spec/**/*.rb
41
+ - lib/apartment/adapters/postgresql_adapter.rb
42
+
43
+ Metrics/ClassLength:
44
+ Max: 155
25
45
  Exclude:
26
46
  - spec/**/*.rb
27
47
 
@@ -76,4 +96,75 @@ Style/CollectionMethods:
76
96
  collect!: 'map!'
77
97
  inject: 'reduce'
78
98
  detect: 'detect'
79
- find_all: 'select'
99
+ find_all: 'select'
100
+
101
+ # RSpec style preferences - disable for mature test suite
102
+ RSpec/NamedSubject:
103
+ Enabled: false
104
+
105
+ RSpec/MultipleExpectations:
106
+ Enabled: false
107
+
108
+ RSpec/MessageSpies:
109
+ Enabled: false
110
+
111
+ RSpec/NestedGroups:
112
+ Enabled: false
113
+
114
+ RSpec/ContextWording:
115
+ Enabled: false
116
+
117
+ RSpec/ExampleLength:
118
+ Enabled: false
119
+
120
+ RSpec/InstanceVariable:
121
+ Enabled: false
122
+
123
+ RSpec/SpecFilePathFormat:
124
+ Enabled: false
125
+
126
+ RSpec/DescribeClass:
127
+ Enabled: false
128
+
129
+ RSpec/IndexedLet:
130
+ Enabled: false
131
+
132
+ RSpec/AnyInstance:
133
+ Enabled: false
134
+
135
+ RSpec/BeforeAfterAll:
136
+ Enabled: false
137
+
138
+ RSpec/LeakyConstantDeclaration:
139
+ Enabled: false
140
+
141
+ RSpec/VerifiedDoubles:
142
+ Enabled: false
143
+
144
+ RSpec/NoExpectationExample:
145
+ Enabled: false
146
+
147
+ # ThreadSafety - intentional design for configuration
148
+ ThreadSafety/ClassInstanceVariable:
149
+ Exclude:
150
+ - lib/apartment.rb
151
+ - lib/apartment/model.rb
152
+ - lib/apartment/elevators/*.rb
153
+ - spec/support/config.rb
154
+
155
+ ThreadSafety/ClassAndModuleAttributes:
156
+ Exclude:
157
+ - lib/apartment.rb
158
+ - lib/apartment/active_record/postgresql_adapter.rb
159
+
160
+ ThreadSafety/DirChdir:
161
+ Exclude:
162
+ - ros-apartment.gemspec
163
+
164
+ ThreadSafety/NewThread:
165
+ Exclude:
166
+ - spec/tenant_spec.rb
167
+
168
+ # Rake cops
169
+ Rake/DuplicateTask:
170
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.6
1
+ 3.3.10
data/Appraisals CHANGED
@@ -128,3 +128,18 @@ appraise 'rails-8-0-sqlite3' do
128
128
  gem 'rails', '~> 8.0.0'
129
129
  gem 'sqlite3', '~> 2.1'
130
130
  end
131
+
132
+ appraise 'rails-8-1-postgresql' do
133
+ gem 'rails', '~> 8.1.0'
134
+ gem 'pg', '~> 1.6.0'
135
+ end
136
+
137
+ appraise 'rails-8-1-mysql' do
138
+ gem 'rails', '~> 8.1.0'
139
+ gem 'mysql2', '~> 0.5'
140
+ end
141
+
142
+ appraise 'rails-8-1-sqlite3' do
143
+ gem 'rails', '~> 8.1.0'
144
+ gem 'sqlite3', '~> 2.8'
145
+ end
data/CLAUDE.md ADDED
@@ -0,0 +1,210 @@
1
+ # CLAUDE.md - Apartment v3 Understanding Guide
2
+
3
+ **Version**: 3.x (Current Development Branch)
4
+ **Maintained by**: CampusESP
5
+ **Gem Name**: `ros-apartment`
6
+
7
+ ## What This Documentation Covers
8
+
9
+ This branch contains v3 (current stable release). A v4 refactor with different architecture exists on `man/spec-restart` branch.
10
+
11
+ **Goal**: Understand v3 deeply enough to maintain it and plan v4 migration.
12
+
13
+ ## Where to Start
14
+
15
+ 1. **README.md** - Installation, basic usage, configuration options
16
+ 2. **docs/architecture.md** - Core design decisions and WHY they were made
17
+ 3. **docs/adapters.md** - Database strategy trade-offs
18
+ 4. **docs/elevators.md** - Middleware design rationale
19
+ 5. **lib/apartment/CLAUDE.md** - Implementation file guide
20
+ 6. **spec/CLAUDE.md** - Test organization and patterns
21
+
22
+ ## Core Concepts
23
+
24
+ ### Multi-Tenancy via Database Isolation
25
+
26
+ **Problem**: Single application needs to serve multiple customers with data completely separated.
27
+
28
+ **v3 Solution**: Thread-local tenant switching. Each request/thread tracks which tenant it's serving.
29
+
30
+ **Key limitation**: Not fiber-safe (fibers share thread-local storage).
31
+
32
+ ### Two Main Strategies
33
+
34
+ **PostgreSQL (schemas)**: Multiple namespaces in single database. Fast, scales to 100+ tenants.
35
+
36
+ **MySQL (databases)**: Separate database per tenant. Complete isolation, slower switching.
37
+
38
+ **See**: `docs/adapters.md` for trade-offs.
39
+
40
+ ### Automatic Tenant Detection
41
+
42
+ **Middleware ("Elevators")**: Rack middleware extracts tenant from request (subdomain, domain, header).
43
+
44
+ **Critical**: Must position before session middleware to avoid data leakage.
45
+
46
+ **See**: `docs/elevators.md` for design decisions.
47
+
48
+ ## Key Architecture Decisions
49
+
50
+ ### 1. Thread-Local Adapter Storage
51
+
52
+ **Why**: Concurrent requests need isolated tenant contexts without global locks.
53
+
54
+ **Implementation**: `Thread.current[:apartment_adapter]`
55
+
56
+ **Trade-off**: Not fiber-safe, but works for 99% of Rails deployments.
57
+
58
+ **See**: `Apartment::Tenant.adapter` method in `tenant.rb`, `docs/architecture.md`
59
+
60
+ ### 2. Block-Based Tenant Switching
61
+
62
+ **Why**: Automatic cleanup even on exceptions prevents tenant context leakage.
63
+
64
+ **Pattern**: `Apartment::Tenant.switch(tenant) { ... }` with ensure block
65
+
66
+ **Alternative rejected**: Manual switch/reset - too error-prone.
67
+
68
+ **See**: `AbstractAdapter#switch` method in `adapters/abstract_adapter.rb`
69
+
70
+ ### 3. Excluded Models
71
+
72
+ **Why**: Some models (User, Company) exist globally across all tenants.
73
+
74
+ **Implementation**: Separate connections that bypass tenant switching.
75
+
76
+ **Limitation**: Can't use `has_and_belongs_to_many` - must use `has_many :through`.
77
+
78
+ **See**: `AbstractAdapter#process_excluded_models` method in `adapters/abstract_adapter.rb`
79
+
80
+ ### 4. Adapter Pattern
81
+
82
+ **Why**: PostgreSQL uses schemas, MySQL uses databases - fundamentally different.
83
+
84
+ **Implementation**: Abstract base class with database-specific subclasses.
85
+
86
+ **Benefit**: Unified API hides database differences.
87
+
88
+ **See**: `lib/apartment/adapters/`, `docs/adapters.md`
89
+
90
+ ### 5. Callback System
91
+
92
+ **Why**: Users need logging/notification hooks without modifying gem code.
93
+
94
+ **Implementation**: ActiveSupport::Callbacks on `:create` and `:switch`.
95
+
96
+ **See**: Callback definitions in `AbstractAdapter` class in `adapters/abstract_adapter.rb`
97
+
98
+ ## File Organization
99
+
100
+ **Core logic**: `lib/apartment.rb` (configuration), `lib/apartment/tenant.rb` (public API)
101
+
102
+ **Adapters**: `lib/apartment/adapters/*.rb` - Database-specific implementations
103
+
104
+ **Elevators**: `lib/apartment/elevators/*.rb` - Rack middleware for auto-switching
105
+
106
+ **Tests**: `spec/` - Adapter tests, elevator tests, integration tests
107
+
108
+ **See folder CLAUDE.md files for details on each directory.**
109
+
110
+ ## Configuration Philosophy
111
+
112
+ **Dynamic tenant discovery**: `tenant_names` can be callable (proc/lambda) that queries database. Why? Tenants change at runtime.
113
+
114
+ **Fail-safe boot**: Rescue database errors during config loading. Why? App should start even if tenant table doesn't exist yet (pending migrations).
115
+
116
+ **Environment isolation**: Optional `prepend_environment`/`append_environment` to prevent cross-environment tenant name collisions.
117
+
118
+ **See**: `Apartment.extract_tenant_config` method in `lib/apartment.rb`
119
+
120
+ ## Common Pitfalls
121
+
122
+ **Elevator positioning**: Must be before session/auth middleware. Otherwise session data leaks across tenants.
123
+
124
+ **Not using blocks**: `switch!` without block requires manual cleanup. Easy to forget. Always prefer `switch` with block.
125
+
126
+ **HABTM with excluded models**: Doesn't work. Must use `has_many :through` instead.
127
+
128
+ **Assuming fiber safety**: v3 uses thread-local storage. Not safe for fiber-based async frameworks.
129
+
130
+ **See**: `docs/architecture.md` for detailed analysis
131
+
132
+ ## Performance Characteristics
133
+
134
+ **PostgreSQL schemas**:
135
+ - Switch: <1ms
136
+ - Scalability: 100+ tenants
137
+ - Memory: Constant
138
+
139
+ **MySQL databases**:
140
+ - Switch: 10-50ms
141
+ - Scalability: 10-50 tenants
142
+ - Memory: Linear with active tenants
143
+
144
+ **See**: `docs/adapters.md` for benchmarks and trade-offs
145
+
146
+ ## Testing the Gem
147
+
148
+ **Spec organization**: `spec/adapters/` for database tests, `spec/unit/elevators/` for middleware tests
149
+
150
+ **Database selection**: `DB=postgresql rspec` or `DB=mysql` or `DB=sqlite3`
151
+
152
+ **Key test pattern**: Create test tenant, switch to it, verify isolation, cleanup
153
+
154
+ **See**: `spec/CLAUDE.md` for testing patterns
155
+
156
+ ## Debugging Techniques
157
+
158
+ **Check current tenant**: `Apartment::Tenant.current`
159
+
160
+ **Inspect adapter**: `Apartment::Tenant.adapter.class`
161
+
162
+ **List tenants**: `Apartment.tenant_names`
163
+
164
+ **Enable logging**: `config.active_record_log = true`
165
+
166
+ **PostgreSQL search path**: `SHOW search_path` in SQL console
167
+
168
+ **See**: Inline code comments for context-specific debugging
169
+
170
+ ## Migration to v4
171
+
172
+ **v4 branch**: `man/spec-restart`
173
+
174
+ **Major changes**: Connection pool per tenant (vs thread-local switching), fiber-safe via CurrentAttributes, immutable connection descriptors
175
+
176
+ **Why v4**: Better performance (no switching overhead), true fiber safety, simpler mental model
177
+
178
+ **Migration strategy**: Understand v3 architecture first (this branch), then contrast with v4 approach
179
+
180
+ ## Design Principles
181
+
182
+ **Open for extension**: Users can create custom adapters and elevators without modifying gem.
183
+
184
+ **Closed for modification**: Core logic shouldn't need changes for new use cases.
185
+
186
+ **Fail fast**: Configuration errors raise at boot. Tenant not found raises at runtime.
187
+
188
+ **Graceful degradation**: If rollback fails, fall back to default tenant rather than crash.
189
+
190
+ **See**: `docs/architecture.md` for rationale
191
+
192
+ ## Getting Help
193
+
194
+ **Issues**: https://github.com/rails-on-services/apartment/issues
195
+
196
+ **Discussions**: https://github.com/rails-on-services/apartment/discussions
197
+
198
+ **Code**: Read the actual implementation files - they're well-commented
199
+
200
+ ## Documentation Philosophy
201
+
202
+ **This documentation focuses on WHY, not HOW**:
203
+ - Design decisions and trade-offs
204
+ - Architecture rationale
205
+ - Pitfalls and constraints
206
+ - References to actual source files
207
+
208
+ **For HOW (implementation details)**: Read the well-commented source code in `lib/`.
209
+
210
+ **For WHAT (API reference)**: See README.md and RDoc comments.
data/README.md CHANGED
@@ -348,7 +348,7 @@ Please note that our custom logger inherits from `ActiveRecord::LogSubscriber` s
348
348
 
349
349
  **Example log output:**
350
350
 
351
- <img src="documentation/images/log_example.png">
351
+ <img src="docs/images/log_example.png">
352
352
 
353
353
  ```ruby
354
354
  Apartment.configure do |config|
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  begin
4
- require 'bundler'
4
+ require('bundler')
5
5
  rescue StandardError
6
6
  'You must `gem install bundler` and `bundle install` to run rake tasks'
7
7
  end
@@ -26,6 +26,7 @@ namespace :spec do
26
26
  end
27
27
  end
28
28
 
29
+ desc 'Start an interactive console with Apartment loaded'
29
30
  task :console do
30
31
  require 'pry'
31
32
  require 'apartment'
@@ -39,15 +40,15 @@ namespace :db do
39
40
  namespace :test do
40
41
  case ENV.fetch('DATABASE_ENGINE', nil)
41
42
  when 'postgresql'
42
- task prepare: %w[postgres:drop_db postgres:build_db]
43
+ task(prepare: %w[postgres:drop_db postgres:build_db])
43
44
  when 'mysql'
44
- task prepare: %w[mysql:drop_db mysql:build_db]
45
+ task(prepare: %w[mysql:drop_db mysql:build_db])
45
46
  when 'sqlite'
46
- task :prepare do
47
+ task(:prepare) do
47
48
  puts 'No need to prepare sqlite3 database'
48
49
  end
49
50
  else
50
- task :prepare do
51
+ task(:prepare) do
51
52
  puts 'No database engine specified, skipping db:test:prepare'
52
53
  end
53
54
  end
data/docs/adapters.md ADDED
@@ -0,0 +1,177 @@
1
+ # Apartment Adapters - Design & Architecture
2
+
3
+ **Key files**: `lib/apartment/adapters/*.rb`
4
+
5
+ ## Purpose
6
+
7
+ Adapters translate abstract tenant operations into database-specific implementations. Each database has fundamentally different isolation mechanisms, requiring separate strategies.
8
+
9
+ ## Design Decision: Why Adapter Pattern?
10
+
11
+ **Problem**: PostgreSQL uses schemas, MySQL uses databases, SQLite uses files. A unified API across these different approaches requires abstraction.
12
+
13
+ **Solution**: Adapter pattern with shared base class defining lifecycle, database-specific subclasses implementing mechanics.
14
+
15
+ **Trade-off**: Adds complexity but enables multi-database support without polluting core logic.
16
+
17
+ ## Adapter Hierarchy
18
+
19
+ See `lib/apartment/adapters/` for implementations:
20
+ - `abstract_adapter.rb` - Shared lifecycle, callbacks, error handling
21
+ - `postgresql_adapter.rb` - Schema-based isolation (3 variants)
22
+ - `mysql2_adapter.rb` - Database-per-tenant
23
+ - `sqlite3_adapter.rb` - File-per-tenant
24
+ - JDBC variants for JRuby
25
+
26
+ ## AbstractAdapter - Design Rationale
27
+
28
+ **File**: `lib/apartment/adapters/abstract_adapter.rb`
29
+
30
+ ### Why Callbacks?
31
+
32
+ Provides extension points for logging, notifications, analytics without modifying core adapter code. Users can hook into `:create` and `:switch` events.
33
+
34
+ ### Why Ensure Blocks in switch()?
35
+
36
+ **Critical decision**: Always rollback to previous tenant, even if block raises. Prevents tenant context leakage across requests/jobs. If rollback fails, fall back to default tenant as last resort.
37
+
38
+ **Alternative considered**: Let exceptions propagate without cleanup. Rejected because it leaves connections in wrong tenant state.
39
+
40
+ ### Why Query Cache Management?
41
+
42
+ Rails disables query cache during connection establishment. Must explicitly preserve and restore state across tenant switches to maintain performance.
43
+
44
+ ### Why Separate Connection Handler?
45
+
46
+ `SeparateDbConnectionHandler` prevents admin operations (CREATE/DROP DATABASE) from polluting the main application connection pool. Multi-server setups especially need this isolation.
47
+
48
+ ## PostgreSQL Adapters - Three Strategies
49
+
50
+ **Files**: `postgresql_adapter.rb` (3 classes in one file)
51
+
52
+ ### 1. PostgresqlAdapter (Database-per-tenant)
53
+
54
+ Rarely used. Most deployments use schemas instead.
55
+
56
+ ### 2. PostgresqlSchemaAdapter (Schema-based - Primary)
57
+
58
+ **Why schemas?**: Single database, multiple namespaces. Fast switching via `SET search_path`. Scales to hundreds of tenants without connection overhead.
59
+
60
+ **Key design decisions**:
61
+ - **Search path ordering**: Tenant schema first, then persistent schemas, then public. Tables resolve in order.
62
+ - **Persistent schemas**: Shared extensions (PostGIS, uuid-ossp) remain accessible across all tenants.
63
+ - **Excluded model handling**: Explicitly qualify table names with default schema to prevent tenant-based queries.
64
+
65
+ **Trade-off**: Less isolation than separate databases, but massively better performance and scalability.
66
+
67
+ ### 3. PostgresqlSchemaFromSqlAdapter (pg_dump-based)
68
+
69
+ **Why pg_dump instead of schema.rb?**:
70
+ - Handles PostgreSQL-specific features (extensions, custom types, constraints) that Rails schema dumper misses
71
+ - Required for PostGIS spatial types
72
+ - Necessary for complex production schemas
73
+
74
+ **Why patch search_path in dump?**: pg_dump outputs assume specific search_path. Must rewrite SQL to target new tenant schema instead of source schema.
75
+
76
+ **Why environment variable handling?**: pg_dump shell command reads PGHOST/PGUSER/etc from ENV. Must temporarily set, execute, then restore to avoid polluting global state.
77
+
78
+ **Alternative considered**: Use Rails schema.rb. Rejected because it loses PostgreSQL-specific features.
79
+
80
+ ## MySQL Adapters - Database Isolation
81
+
82
+ **Files**: `mysql2_adapter.rb`, `trilogy_adapter.rb`
83
+
84
+ ### Why Separate Databases?
85
+
86
+ MySQL doesn't have PostgreSQL's robust schema support. Database-level isolation is the natural fit.
87
+
88
+ **Implications**:
89
+ - Each switch establishes new connection to different database
90
+ - Connection pool per tenant (memory overhead)
91
+ - Practical limit of 10-50 concurrent tenants before connection exhaustion
92
+
93
+ ### Why Trilogy Adapter?
94
+
95
+ Modern MySQL driver. Identical implementation to Mysql2Adapter, just different gem.
96
+
97
+ ### Multi-Server Support
98
+
99
+ Hash-based tenant config allows different tenants on different MySQL servers. Enables horizontal scaling and geographic distribution.
100
+
101
+ ## SQLite Adapter - File-Based
102
+
103
+ **File**: `sqlite3_adapter.rb`
104
+
105
+ ### Why File-Per-Tenant?
106
+
107
+ SQLite is single-file database. Natural isolation is separate files.
108
+
109
+ **Use case**: Testing, development, single-user apps. **Not** production multi-tenant.
110
+
111
+ ## Performance Characteristics
112
+
113
+ **PostgreSQL schemas**:
114
+ - Switch latency: <1ms (SQL command)
115
+ - Scalability: 100+ tenants easily
116
+ - Memory: Constant (~50MB)
117
+
118
+ **MySQL databases**:
119
+ - Switch latency: 10-50ms (connection establishment)
120
+ - Scalability: 10-50 tenants
121
+ - Memory: ~20MB per active tenant
122
+
123
+ **SQLite files**:
124
+ - Switch latency: 5-20ms (file I/O)
125
+ - Scalability: Not recommended for concurrent users
126
+ - Memory: ~5MB per database
127
+
128
+ ## Adapter Selection Matrix
129
+
130
+ | Database | Strategy | Speed | Scalability | Isolation | Best For |
131
+ |------------|--------------|-----------|-------------|-----------|-----------------------|
132
+ | PostgreSQL | Schemas | Very Fast | Excellent | Good | 100+ tenants |
133
+ | MySQL | Databases | Moderate | Good | Excellent | Complete isolation |
134
+ | SQLite | Files | Moderate | Poor | Excellent | Testing only |
135
+
136
+ ## Extension Points
137
+
138
+ ### Creating Custom Adapters
139
+
140
+ **Why you might need this**: Supporting databases not yet implemented (Oracle, SQL Server, CockroachDB).
141
+
142
+ **What to implement**:
143
+ 1. Subclass `AbstractAdapter`
144
+ 2. Define required methods: `create_tenant`, `connect_to_new`, `drop_command`, `current`
145
+ 3. Register factory method in `lib/apartment/tenant.rb`
146
+
147
+ **See**: Existing adapters for patterns. PostgreSQL is most complex, SQLite is simplest.
148
+
149
+ ## Common Pitfalls & Design Constraints
150
+
151
+ ### Why Transaction Handling in create_tenant?
152
+
153
+ RSpec tests run in transactions. Must detect open transactions and avoid nested BEGIN/COMMIT to prevent errors.
154
+
155
+ ### Why Separate rescue_from per Adapter?
156
+
157
+ Different databases raise different exceptions. PostgreSQL raises `PG::Error`, MySQL raises different errors. Each adapter specifies what to rescue.
158
+
159
+ ### Why environmentify()?
160
+
161
+ Prevents tenant name collisions across Rails environments. `development_acme` vs `production_acme`. Optional but recommended for shared infrastructure.
162
+
163
+ ## Thread Safety
164
+
165
+ **Critical**: Adapters stored in `Thread.current[:apartment_adapter]`. Each thread gets isolated adapter instance.
166
+
167
+ **Implication**: Safe for multi-threaded servers (Puma), background jobs (Sidekiq).
168
+
169
+ **Limitation**: Not fiber-safe. v4 refactor addresses this.
170
+
171
+ ## References
172
+
173
+ - AbstractAdapter implementation: `lib/apartment/adapters/abstract_adapter.rb`
174
+ - PostgreSQL variants: `lib/apartment/adapters/postgresql_adapter.rb`
175
+ - MySQL variants: `lib/apartment/adapters/mysql2_adapter.rb`, `trilogy_adapter.rb`
176
+ - SQLite: `lib/apartment/adapters/sqlite3_adapter.rb`
177
+ - PostgreSQL documentation: https://www.postgresql.org/docs/current/ddl-schemas.html