Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
98169fd
Move tests into adapter folders
andrewmarkle Oct 29, 2025
ea0e14d
Add mysql and a devcontainer to easily run it
andrewmarkle Oct 29, 2025
8466298
Add mysql adapter with tests
andrewmarkle Oct 29, 2025
d975bd4
Simplify devcontainer setup
andrewmarkle Oct 29, 2025
bbd4e53
Clean up the mysql adapter
andrewmarkle Oct 29, 2025
4445329
Refactor
andrewmarkle Oct 30, 2025
1b87d51
Update integration script
andrewmarkle Oct 30, 2025
1c6eda2
Add adapter folder and get sqlite integration tests to pass
andrewmarkle Oct 30, 2025
94e6d86
Add chrome to devcontainer so that we can run integration tests in it
andrewmarkle Oct 30, 2025
132e9c6
A couple fixes. Switch to mysql
andrewmarkle Oct 30, 2025
9da4474
Fix database tasks
andrewmarkle Nov 3, 2025
ef0819d
Fix tests for sqlite
andrewmarkle Nov 3, 2025
5929062
Make fake database creation work for all adapters
andrewmarkle Nov 3, 2025
e980a15
shorten the db name so it's shorter than the mysql max (64)
andrewmarkle Nov 3, 2025
6c9121b
Working(ish) integration tests with mysql
andrewmarkle Nov 3, 2025
1b147f0
Specify RAILS_ENV
andrewmarkle Nov 4, 2025
ce9f288
Refactor and try to make test_workerize work
andrewmarkle Nov 4, 2025
7b28b4b
Isolate the dbs for integration tests
andrewmarkle Nov 4, 2025
f69a5c1
Make sure dbs start from a clean state everytime
andrewmarkle Nov 4, 2025
bda859a
Enable integration tests for mysql and sqlite
andrewmarkle Nov 4, 2025
7152457
Add mysql schema_cache and make sure the unit tests use it
andrewmarkle Nov 5, 2025
d47aa8c
Refactor cleaning up databases in unit tests
andrewmarkle Nov 5, 2025
8418c24
Rename methods
andrewmarkle Nov 5, 2025
31fe774
Simplify SystemTest setup
andrewmarkle Nov 5, 2025
e004cd9
Revert un-needed change
andrewmarkle Nov 5, 2025
b22fa30
More cleanup
andrewmarkle Nov 5, 2025
6f46023
Simplify
andrewmarkle Nov 5, 2025
798c40d
Add test to ensure shared dbs don't get passed to adapter
andrewmarkle Nov 5, 2025
84fd830
Add tests to ensure we get a valid mysql database name
andrewmarkle Nov 5, 2025
bc76b0e
Add tests for when a host option is provided and more cleanup
andrewmarkle Nov 5, 2025
1027b04
Move comment
andrewmarkle Nov 5, 2025
8ce81ff
Refactor test-integration setup for mysql
andrewmarkle Nov 6, 2025
bb2b311
Simplify migration logic
andrewmarkle Nov 6, 2025
6431651
Improve the test coverage of database_tasks
andrewmarkle Nov 6, 2025
554f378
Refactor and clean up
andrewmarkle Nov 6, 2025
c9c421a
Run both types of adapters in the integration tests
andrewmarkle Nov 6, 2025
c47579c
Reject the connection if the tenant is an empty string
andrewmarkle Nov 6, 2025
18ca09c
Configure CI to add mysql
andrewmarkle Nov 6, 2025
cb16d87
Align testing adapters more closely and add more test scenarios to mysql
andrewmarkle Nov 6, 2025
2e8b21c
Fix test and ensure temp connection is more obviously set (and exclude
andrewmarkle Nov 7, 2025
3c055d5
Ensure unit test only runs for sqlite
andrewmarkle Nov 7, 2025
eabf8c8
Fix mismatch in system tests for action cable setup
andrewmarkle Nov 7, 2025
ed0539c
Try explicitly busting the cache
andrewmarkle Nov 7, 2025
76c2acf
Updo the cache busting and add some debug info
andrewmarkle Nov 7, 2025
41e1f23
Try this
andrewmarkle Nov 7, 2025
745df5d
Try to fix the debug methods
andrewmarkle Nov 7, 2025
d238e6b
Cleanup fix for test
andrewmarkle Nov 7, 2025
d8d75c9
Refactor temp_connection name
andrewmarkle Nov 14, 2025
1f8804b
Remove dev containers to prefer a bin/setup script
andrewmarkle Nov 21, 2025
7b09a46
Add PostgreSQL Support
Dreamer009 Dec 18, 2025
6ea0ec3
Change the test framework. Keeping in a seperate commit incase I need…
Dreamer009 Dec 18, 2025
ff3063b
Fix test-integration for postgresql by commiting the db transaction w…
Dreamer009 Dec 18, 2025
730356c
Add postgresql_strategy option in database.yml to choose between sche…
Dreamer009 Dec 18, 2025
f971ea5
Add documentation for PostgreSQL tenant name constraints
Dreamer009 Dec 19, 2025
1d42719
Allow PosgreSQL schema to drop entire colacated database
Dreamer009 Dec 19, 2025
3aaa5c3
Create colocated adapter and added rake db:create functionality
Dreamer009 Dec 19, 2025
95745ce
Fix PendingMigrationError
Dreamer009 Dec 20, 2025
db01981
Change configuration to use schema_name_pattern instead of postgresql…
Dreamer009 Dec 21, 2025
004cbb0
Reoganize postgresql test senarios
Dreamer009 Dec 21, 2025
50c2545
Add validation to prevent postgresql schema_name_pattern with dynamic…
Dreamer009 Dec 21, 2025
2806399
Fix PostgreSQL issues from running inside a transaction + cable and t…
Dreamer009 Dec 21, 2025
ef50cb3
Add configuration for PostgreSQL maintenance DB
Dreamer009 Dec 25, 2025
e48a820
Change from schema_name_pattern config to using schema config for all…
Dreamer009 Dec 25, 2025
112e752
Remove double 'acount-' tagging and default schema tenant in colocate…
Dreamer009 Dec 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,84 @@ jobs:

unit:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: devcontainer
MYSQL_DATABASE: test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping --silent"
--health-interval=10s
--health-timeout=5s
--health-retries=3
postgres:
image: postgres:latest
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0
with:
ruby-version: "3.4"
bundler-cache: true
- run: bin/test-unit
env:
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: "3306"
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: "5432"
POSTGRES_USERNAME: postgres
POSTGRES_PASSWORD: postgres

integration:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: devcontainer
MYSQL_DATABASE: test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping --silent"
--health-interval=10s
--health-timeout=5s
--health-retries=3
postgres:
image: postgres:latest
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0
with:
ruby-version: "3.4"
bundler-cache: true
- run: bin/test-integration
env:
MYSQL_HOST: 127.0.0.1
MYSQL_PORT: "3306"
POSTGRES_HOST: 127.0.0.1
POSTGRES_PORT: "5432"
POSTGRES_USERNAME: postgres
POSTGRES_PASSWORD: postgres
149 changes: 149 additions & 0 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,138 @@ class ApplicationRecord < ActiveRecord::Base
end
```

#### 2.2.1 PostgreSQL Multi-Tenancy Strategies

PostgreSQL supports two isolation strategies, automatically inferred from the database name configuration.

##### Schema-Based Multi-Tenancy

Uses PostgreSQL schemas within a single database. This is the recommended strategy for most use cases.

**Configuration:**

``` yaml
production:
primary:
adapter: postgresql
database: myapp_production # Static database name
tenanted: true
host: localhost
```

In this configuration:
- A single PostgreSQL database named `myapp_production` is created (static name)
- Each tenant gets its own schema with the prefix `account-` (e.g., `account-tenant1`, `account-tenant2`)
- The `schema_search_path` is set automatically to isolate tenants
- All tables and data are stored within the tenant-specific schema
- **Auto-detection:** Automatically used when database name does NOT contain `%{tenant}`

**Advantages:**
- **Resource Efficient**: Single database process serves all tenants
- **Lower Memory Usage**: Shared buffer cache across tenants
- **Simpler Backup**: One database to backup/restore
- **Better for Scale**: Supports thousands of tenants efficiently
- **PostgreSQL Best Practice**: Aligns with PostgreSQL's schema design

**Considerations:**
- Connection limits are shared across all tenants
- Schema-level isolation (not database-level)
- All tenants must use the same PostgreSQL version/settings

##### Database-Based Multi-Tenancy

Creates separate PostgreSQL databases for each tenant. Similar to how MySQL and SQLite work in this gem.

**Configuration:**

``` yaml
production:
primary:
adapter: postgresql
database: "%{tenant}"
tenanted: true
host: localhost
```

In this configuration:
- Each tenant gets its own PostgreSQL database: `account_foo`, `account_bar`, etc.
- Each database has independent schemas, users, and settings
- Complete isolation between tenants at the database level
- **Auto-detection:** Automatically used when database name contains `%{tenant}`

**Advantages:**
- **Stronger Isolation**: Complete database-level separation
- **Independent Configuration**: Each tenant can have different settings
- **Easier Data Export**: Simple to dump/restore individual tenants
- **Familiar Pattern**: Consistent with MySQL/SQLite behavior

**Considerations:**
- Higher resource usage (more database processes)
- Higher memory usage (separate buffer cache per database)
- More complex backup/restore operations
- PostgreSQL may have limits on number of databases
- Not recommended for hundreds of tenants

##### Performance Comparison

| Metric | Schema Strategy | Database Strategy |
|--------|----------------|-------------------|
| **Connection Overhead** | Low (single DB) | Medium (multiple DBs) |
| **Memory Usage** | Low (shared cache) | High (cache per DB) |
| **Query Performance** | Excellent | Excellent |
| **Tenant Isolation** | Schema-level | Database-level |
| **Backup/Restore** | Simple (one DB) | Complex (many DBs) |
| **Tenant Limit** | Thousands | Hundreds |
| **Resource Efficiency** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| **Data Isolation** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |

##### Choosing a Strategy

**Use Schema Strategy (recommended) when:**
- You have many tenants (dozens to thousands)
- Resource efficiency is important
- You're following PostgreSQL best practices
- Tenants share the same configuration needs
- You want simpler operations (backup, monitoring, etc.)
- Configuration: Use a static database name (e.g., `database: myapp_production`)

**Use Database Strategy when:**
- You have few tenants (less than 100)
- You need database-level isolation for compliance
- Each tenant needs different database settings
- You need to easily export individual tenant databases
- You want consistency with MySQL/SQLite behavior
- Configuration: Use `%{tenant}` in database name (e.g., `database: "%{tenant}"`)

##### Migration Between Strategies

To change strategies, you'll need to:

1. Export data from existing tenants
2. Update `database.yml`:
- For Schema → Database: Change `database: myapp_production` to `database: "%{tenant}"`
- For Database → Schema: Change `database: "%{tenant}"` to `database: myapp_production`
3. Create new tenant databases/schemas
4. Import data into new structure

**Note:** There is no automated migration tool. Plan strategy choice carefully before production deployment.

##### PostgreSQL Tenant Name Constraints

PostgreSQL has strict naming conventions for identifiers (database names and schema names). When using PostgreSQL with this gem, tenant names are subject to the following constraints:

**Allowed Characters:**
- Letters (a-z, A-Z)
- Numbers (0-9)
- Underscores (`_`)
- Dollar signs (`$`)
- Hyphens (`-`)

**Additional Constraints:**
- **Maximum Length:** 63 characters total (including the `account-` prefix for schema strategy)
- **First Character:** Must be a letter or underscore (cannot start with a number or special character)
- **Forward Slashes:** Not allowed in PostgreSQL identifiers

### 2.3 Configuring `max_connection_pools`

By default, Active Record Tenanted will cap the number of tenanted connection pools to 50. Setting a limit on the number of "live" connection pools at any one time provides control over the number of file descriptors used for database connections. For SQLite databases, it's also an important control on the amount of memory used.
Expand All @@ -290,6 +422,23 @@ production:
Active Record Tenanted will reap the least-recently-used connection pools when this limit is surpassed. Developers are encouraged to tune this parameter with care, since setting it too low may lead to increased request latency due to frequently re-establishing database connections, while setting it too high may consume precious file descriptors and memory resources.


### 2.3.1 Configuring PostgreSQL Maintenance Database

PostgreSQL requires connecting to an existing database to perform administrative operations like creating or dropping databases, listing databases, or creating schemas. By default, Active Record Tenanted uses the "postgres" database (the default PostgreSQL system database) for these maintenance operations.

You can customize which database is used for maintenance operations by setting the `maintenance_database` parameter in `config/database.yml`:

``` yaml
development:
primary:
adapter: postgresql
database: myapp_development
tenanted: true
maintenance_database: myapp_maintenance_development
```

**Note:** The maintenance database must exist and be accessible by your database user before running any tenant operations. This setting applies to both schema-based and database-based multi-tenancy strategies.

### 2.4 Configuring the Connection Class

By default, Active Record Tenanted assumes that `ApplicationRecord` is the tenanted abstract base class:
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ group :development, :test do
gem "sqlite3", "2.7.4"
gem "debug", "1.11.0"
gem "minitest-parallel_fork", "2.1.0", require: false
gem "mysql2", "~> 0.5", require: false
gem "pg", "~> 1.1", require: false
end

group :rubocop do
Expand Down
8 changes: 8 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ GEM
minitest (5.26.0)
minitest-parallel_fork (2.1.0)
minitest (>= 5.15.0)
mysql2 (0.5.7)
bigdecimal
net-imap (0.5.12)
date
net-protocol
Expand All @@ -199,6 +201,10 @@ GEM
parser (3.3.9.0)
ast (~> 2.4.1)
racc
pg (1.6.2-arm64-darwin)
pg (1.6.2-x86_64-darwin)
pg (1.6.2-x86_64-linux)
pg (1.6.2-x86_64-linux-musl)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
Expand Down Expand Up @@ -349,6 +355,8 @@ DEPENDENCIES
importmap-rails
jbuilder
minitest-parallel_fork (= 2.1.0)
mysql2 (~> 0.5)
pg (~> 1.1)
propshaft
puma (>= 5.0)
rails!
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Enable a Rails application to host multiple isolated tenants.

> [!NOTE]
> Only the sqlite3 database adapter is fully supported right now. If you have a use case for tenanting one of the other databases supported by Rails, please reach out to the maintainers!
> Currently supported database adapters: SQLite3, MySQL (mysql2/trilogy), and PostgreSQL. PostgreSQL supports both schema-based (default) and database-based multi-tenancy strategies. If you have a use case for tenanting other databases supported by Rails, please reach out to the maintainers!

## Summary

Expand Down
Loading