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
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Apartment
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/ros-apartment)
|
|
4
|
-
[](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
|
|
12
|
+
## Apartment Fork: ros-apartment
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
##
|
|
16
|
+
## Community Support
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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="
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
663
|
+
### Questions and Support
|
|
645
664
|
|
|
646
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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 :
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
88
|
+
system("createdb #{params.join(' ')}")
|
|
71
89
|
rescue StandardError
|
|
72
90
|
'test db already exists'
|
|
73
91
|
end
|
|
74
|
-
ActiveRecord::Base.establish_connection
|
|
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
|
-
|
|
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 << "
|
|
117
|
+
params << "-p #{my_config['password']}" if my_config['password']
|
|
118
|
+
params << "-P #{my_config['port']}" if my_config['port']
|
|
101
119
|
begin
|
|
102
|
-
|
|
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
|
|
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 << "
|
|
118
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|