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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +142 -8
  4. data/.ruby-version +1 -1
  5. data/Appraisals +125 -30
  6. data/CLAUDE.md +210 -0
  7. data/CODE_OF_CONDUCT.md +71 -0
  8. data/Gemfile +15 -0
  9. data/README.md +49 -31
  10. data/Rakefile +44 -25
  11. data/docs/adapters.md +177 -0
  12. data/docs/architecture.md +274 -0
  13. data/docs/elevators.md +226 -0
  14. data/docs/images/log_example.png +0 -0
  15. data/lib/apartment/CLAUDE.md +300 -0
  16. data/lib/apartment/active_record/connection_handling.rb +2 -2
  17. data/lib/apartment/active_record/postgres/schema_dumper.rb +20 -0
  18. data/lib/apartment/active_record/postgresql_adapter.rb +19 -4
  19. data/lib/apartment/adapters/CLAUDE.md +314 -0
  20. data/lib/apartment/adapters/abstract_adapter.rb +24 -15
  21. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +1 -1
  22. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
  23. data/lib/apartment/adapters/mysql2_adapter.rb +2 -2
  24. data/lib/apartment/adapters/postgresql_adapter.rb +55 -36
  25. data/lib/apartment/adapters/sqlite3_adapter.rb +7 -7
  26. data/lib/apartment/console.rb +1 -1
  27. data/lib/apartment/custom_console.rb +7 -7
  28. data/lib/apartment/deprecation.rb +2 -5
  29. data/lib/apartment/elevators/CLAUDE.md +292 -0
  30. data/lib/apartment/elevators/domain.rb +1 -1
  31. data/lib/apartment/elevators/generic.rb +1 -1
  32. data/lib/apartment/elevators/host_hash.rb +3 -3
  33. data/lib/apartment/elevators/subdomain.rb +9 -5
  34. data/lib/apartment/log_subscriber.rb +1 -1
  35. data/lib/apartment/migrator.rb +17 -5
  36. data/lib/apartment/model.rb +1 -1
  37. data/lib/apartment/railtie.rb +3 -3
  38. data/lib/apartment/tasks/enhancements.rb +1 -1
  39. data/lib/apartment/tasks/task_helper.rb +6 -4
  40. data/lib/apartment/tenant.rb +3 -3
  41. data/lib/apartment/version.rb +1 -1
  42. data/lib/apartment.rb +23 -11
  43. data/lib/generators/apartment/install/install_generator.rb +1 -1
  44. data/lib/generators/apartment/install/templates/apartment.rb +2 -2
  45. data/lib/tasks/apartment.rake +25 -25
  46. data/ros-apartment.gemspec +10 -35
  47. metadata +44 -245
  48. data/.rubocop_todo.yml +0 -439
  49. /data/{CHANGELOG.md → legacy_CHANGELOG.md} +0 -0
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Apartment
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ros-apartment.svg)](https://badge.fury.io/rb/ros-apartment)
4
- [![Code Climate](https://api.codeclimate.com/v1/badges/b0dc327380bb8438f991/maintainability)](https://codeclimate.com/github/rails-on-services/apartment/maintainability)
4
+ [![codecov](https://codecov.io/gh/rails-on-services/apartment/graph/badge.svg?token=Q4I5QL78SA)](https://codecov.io/gh/rails-on-services/apartment)
5
5
 
6
6
  *Multitenancy for Rails and ActiveRecord*
7
7
 
@@ -9,21 +9,17 @@ Apartment provides tools to help you deal with multiple tenants in your Rails
9
9
  application. If you need to have certain data sequestered based on account or company,
10
10
  but still allow some data to exist in a common tenant, Apartment can help.
11
11
 
12
- ## Apartment drop in replacement gem
12
+ ## Apartment Fork: ros-apartment
13
13
 
14
- After having reached out via github issues and email directly, no replies ever
15
- came. Since we wanted to upgrade our application to Rails 6 we decided to fork
16
- and start some development to support Rails 6. Because we don't have access
17
- to the apartment gem itself, the solution was to release it under a different
18
- name but providing the exact same API as it was before.
14
+ This gem is a fork of the original Apartment gem, which is no longer maintained. We have continued development under the name `ros-apartment` to keep the gem up-to-date and compatible with the latest versions of Rails. `ros-apartment` is designed as a drop-in replacement for the original, allowing you to seamlessly transition your application without code changes.
19
15
 
20
- ## Help wanted
16
+ ## Community Support
21
17
 
22
- We were never involved with the development of Apartment gem in the first place
23
- and this project started out of our own needs. We will be more than happy
24
- to collaborate to maintain the gem alive and supporting the latest versions
25
- of ruby and rails, but your help is appreciated. Either by reporting bugs you
26
- may find or proposing improvements to the gem itself. Feel free to reach out.
18
+ This project thrives on community support. Whether you have an idea for a new feature, find a bug, or need help with `ros-apartment`, we encourage you to participate! For questions and troubleshooting, check out our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to connect with the community. You can also open issues or submit pull requests directly. We are committed to maintaining `ros-apartment` and ensuring it remains a valuable tool for Rails developers.
19
+
20
+ ### Maintainer Update
21
+
22
+ As of May 2024, Apartment is maintained with the support of [CampusESP](https://www.campusesp.com). We continue to keep Apartment open-source under the MIT license. We also want to recognize and thank the previous maintainers for their valuable contributions to this project.
27
23
 
28
24
  ## Installation
29
25
 
@@ -47,10 +43,6 @@ Configure as needed using the docs below.
47
43
  That's all you need to set up the Apartment libraries. If you want to switch tenants
48
44
  on a per-user basis, look under "Usage - Switching tenants per request", below.
49
45
 
50
- > NOTE: If using [postgresql schemas](http://www.postgresql.org/docs/9.0/static/ddl-schemas.html) you must use:
51
- >
52
- > * for Rails 3.1.x: _Rails ~> 3.1.2_, it contains a [patch](https://github.com/rails/rails/pull/3232) that makes prepared statements work with multiple schemas
53
-
54
46
  ## Usage
55
47
 
56
48
  ### Video Tutorial
@@ -356,7 +348,7 @@ Please note that our custom logger inherits from `ActiveRecord::LogSubscriber` s
356
348
 
357
349
  **Example log output:**
358
350
 
359
- <img src="documentation/images/log_example.png">
351
+ <img src="docs/images/log_example.png">
360
352
 
361
353
  ```ruby
362
354
  Apartment.configure do |config|
@@ -630,24 +622,50 @@ $ APARTMENT_DISABLE_INIT=true DATABASE_URL=postgresql://localhost:1234/buk_devel
630
622
  # 1
631
623
  ```
632
624
 
633
- ## Contributing
625
+ ## Contribution Guidelines
626
+
627
+ We welcome and appreciate contributions to `ros-apartment`! Whether you want to report a bug, propose a new feature, or submit a pull request, your help keeps this project thriving. Please review the guidelines below to ensure a smooth collaboration process.
628
+
629
+ ### How to Contribute
630
+
631
+ 1. **Check Existing Issues and Discussions**
632
+ - Before opening a new issue, please check the [issue tracker](https://github.com/rails-on-services/apartment/issues) and our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to see if the topic has already been reported or discussed. This helps us avoid duplication and focus on solving the issue efficiently.
633
+
634
+ 2. **Submitting a Bug Report**
635
+ - Ensure your report includes a clear description of the problem, steps to reproduce, and relevant logs or error messages.
636
+ - If possible, provide a minimal reproducible example or a failing test case that demonstrates the issue.
637
+
638
+ 3. **Proposing a Feature**
639
+ - For new features, open an issue to discuss your idea before starting development. This allows the maintainers and community to provide feedback and ensure the feature aligns with the project's goals.
640
+ - Please be as detailed as possible when describing the feature, its use case, and its potential impact on the existing functionality.
641
+
642
+ 4. **Submitting a Pull Request**
643
+ - Fork the repository and create a feature branch (`git checkout -b my-feature-branch`).
644
+ - Follow the existing code style and ensure your changes are well-documented and tested.
645
+ - Run the tests locally to verify that your changes do not introduce new issues.
646
+ - Use [Appraisal](https://github.com/thoughtbot/appraisal) to test against multiple Rails versions. Ensure all tests pass for supported Rails versions.
647
+ - Submit your pull request to the `development` branch, not `main`.
648
+ - Include a detailed description of your changes and reference any related issue numbers (e.g., "Fixes #123" or "Closes #456").
649
+
650
+ 5. **Code Review and Merging Process**
651
+ - The maintainers will review your pull request and may provide feedback or request changes. We appreciate your patience during this process, as we strive to maintain a high standard for code quality.
652
+ - Once approved, your pull request will be merged into the `development` branch. Periodically, we merge the `development` branch into `main` for official releases.
653
+
654
+ 6. **Testing**
655
+ - Ensure your code is thoroughly tested. We do not merge code changes without adequate tests. Use RSpec for unit and integration tests.
656
+ - If your contribution affects multiple versions of Rails, use Appraisal to verify compatibility across versions.
657
+ - Rake tasks (see the Rakefile) are available to help set up your test databases and run tests.
634
658
 
635
- * In both `spec/dummy/config` and `spec/config`, you will see `database.yml.sample` files
636
- * Copy them into the same directory but with the name `database.yml`
637
- * Edit them to fit your own settings
638
- * Rake tasks (see the Rakefile) will help you setup your dbs necessary to run tests
639
- * Please issue pull requests to the `development` branch. All development happens here, master is used for releases.
640
- * Ensure that your code is accompanied with tests. No code will be merged without tests
659
+ ### Code of Conduct
641
660
 
642
- * If you're looking to help, check out the TODO file for some upcoming changes I'd like to implement in Apartment.
661
+ We are committed to providing a welcoming and inclusive environment for all contributors. Please review and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) when participating in the project.
643
662
 
644
- ### Running bundle install
663
+ ### Questions and Support
645
664
 
646
- mysql2 gem in some cases fails to install.
647
- If you face problems running bundle install in OSX, try installing the gem running:
665
+ If you have any questions or need support while contributing or using `ros-apartment`, visit our [Discussions board](https://github.com/rails-on-services/apartment/discussions) to ask questions and connect with the maintainer team and community.
648
666
 
649
- `gem install mysql2 -v '0.5.3' -- --with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include`
667
+ We look forward to your contributions and thank you for helping us keep `ros-apartment` a reliable and robust tool for the Rails community!
650
668
 
651
669
  ## License
652
670
 
653
- Apartment is released under the [MIT License](http://www.opensource.org/licenses/MIT).
671
+ Apartment remains an open-source project under the [MIT License](http://www.opensource.org/licenses/MIT). We value open-source principles and aim to make multitenancy accessible to all Rails developers.
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
@@ -13,7 +13,7 @@ require 'appraisal'
13
13
  require 'rspec'
14
14
  require 'rspec/core/rake_task'
15
15
 
16
- RSpec::Core::RakeTask.new(spec: %w[db:copy_credentials db:test:prepare]) do |spec|
16
+ RSpec::Core::RakeTask.new(spec: %w[db:load_credentials db:test:prepare]) do |spec|
17
17
  spec.pattern = 'spec/**/*_spec.rb'
18
18
  # spec.rspec_opts = '--order rand:47078'
19
19
  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'
@@ -37,19 +38,36 @@ task default: :spec
37
38
 
38
39
  namespace :db do
39
40
  namespace :test do
40
- task prepare: %w[postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db]
41
+ case ENV.fetch('DATABASE_ENGINE', nil)
42
+ when 'postgresql'
43
+ task(prepare: %w[postgres:drop_db postgres:build_db])
44
+ when 'mysql'
45
+ task(prepare: %w[mysql:drop_db mysql:build_db])
46
+ when 'sqlite'
47
+ task(:prepare) do
48
+ puts 'No need to prepare sqlite3 database'
49
+ end
50
+ else
51
+ task(:prepare) do
52
+ puts 'No database engine specified, skipping db:test:prepare'
53
+ end
54
+ end
41
55
  end
42
56
 
43
57
  desc "copy sample database credential files over if real files don't exist"
44
- task :copy_credentials do
45
- require 'fileutils'
46
- apartment_db_file = 'spec/config/database.yml'
47
- rails_db_file = 'spec/dummy/config/database.yml'
58
+ task :load_credentials do
59
+ # If no DATABASE_ENGINE is specified, we default to sqlite so that a db config is generated
60
+ db_engine = ENV.fetch('DATABASE_ENGINE', 'sqlite')
48
61
 
49
- unless File.exist?(apartment_db_file)
50
- FileUtils.copy("#{apartment_db_file}.sample", apartment_db_file, verbose: true)
51
- end
52
- FileUtils.copy("#{rails_db_file}.sample", rails_db_file, verbose: true) unless File.exist?(rails_db_file)
62
+ next unless db_engine && %w[postgresql mysql sqlite].include?(db_engine)
63
+
64
+ # Load and write spec db config
65
+ db_config_string = ERB.new(File.read("spec/config/#{db_engine}.yml.erb")).result
66
+ File.write('spec/config/database.yml', db_config_string)
67
+
68
+ # Load and write dummy app db config
69
+ db_config = YAML.safe_load(db_config_string)
70
+ File.write('spec/dummy/config/database.yml', { test: db_config['connections'][db_engine] }.to_yaml)
53
71
  end
54
72
  end
55
73
 
@@ -67,11 +85,11 @@ namespace :postgres do
67
85
  params << "-p#{pg_config['port']}" if pg_config['port']
68
86
 
69
87
  begin
70
- `createdb #{params.join(' ')}`
88
+ system("createdb #{params.join(' ')}")
71
89
  rescue StandardError
72
90
  'test db already exists'
73
91
  end
74
- ActiveRecord::Base.establish_connection pg_config
92
+ ActiveRecord::Base.establish_connection(pg_config)
75
93
  migrate
76
94
  end
77
95
 
@@ -83,7 +101,7 @@ namespace :postgres do
83
101
  params << "-U#{pg_config['username']}"
84
102
  params << "-h#{pg_config['host']}" if pg_config['host']
85
103
  params << "-p#{pg_config['port']}" if pg_config['port']
86
- `dropdb #{params.join(' ')}`
104
+ system("dropdb #{params.join(' ')}")
87
105
  end
88
106
  end
89
107
 
@@ -96,14 +114,14 @@ namespace :mysql do
96
114
  params = []
97
115
  params << "-h #{my_config['host']}" if my_config['host']
98
116
  params << "-u #{my_config['username']}" if my_config['username']
99
- params << "-p#{my_config['password']}" if my_config['password']
100
- params << "--port #{my_config['port']}" if my_config['port']
117
+ params << "-p #{my_config['password']}" if my_config['password']
118
+ params << "-P #{my_config['port']}" if my_config['port']
101
119
  begin
102
- `mysqladmin #{params.join(' ')} create #{my_config['database']}`
120
+ system("mysqladmin #{params.join(' ')} create #{my_config['database']}")
103
121
  rescue StandardError
104
122
  'test db already exists'
105
123
  end
106
- ActiveRecord::Base.establish_connection my_config
124
+ ActiveRecord::Base.establish_connection(my_config)
107
125
  migrate
108
126
  end
109
127
 
@@ -113,13 +131,12 @@ namespace :mysql do
113
131
  params = []
114
132
  params << "-h #{my_config['host']}" if my_config['host']
115
133
  params << "-u #{my_config['username']}" if my_config['username']
116
- params << "-p#{my_config['password']}" if my_config['password']
117
- params << "--port #{my_config['port']}" if my_config['port']
118
- `mysqladmin #{params.join(' ')} drop #{my_config['database']} --force`
134
+ params << "-p #{my_config['password']}" if my_config['password']
135
+ params << "-P #{my_config['port']}" if my_config['port']
136
+ system("mysqladmin #{params.join(' ')} drop #{my_config['database']} --force")
119
137
  end
120
138
  end
121
139
 
122
- # TODO: clean this up
123
140
  def config
124
141
  Apartment::Test.config['connections']
125
142
  end
@@ -133,7 +150,9 @@ def my_config
133
150
  end
134
151
 
135
152
  def migrate
136
- # TODO: Figure out if there is any other possibility that can/should be
137
- # passed here as the second argument for the migration context
138
- ActiveRecord::MigrationContext.new('spec/dummy/db/migrate', ActiveRecord::SchemaMigration).migrate
153
+ if ActiveRecord.version.release < Gem::Version.new('7.1')
154
+ ActiveRecord::MigrationContext.new('spec/dummy/db/migrate', ActiveRecord::SchemaMigration).migrate
155
+ else
156
+ ActiveRecord::MigrationContext.new('spec/dummy/db/migrate').migrate
157
+ end
139
158
  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