ros-apartment 3.3.0 → 3.4.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 +6 -0
- data/.ruby-version +1 -1
- data/AGENTS.md +19 -0
- data/README.md +54 -7
- data/lib/apartment/CLAUDE.md +3 -3
- data/lib/apartment/tasks/CLAUDE.md +107 -0
- data/lib/apartment/tasks/schema_dumper.rb +109 -0
- data/lib/apartment/tasks/task_helper.rb +273 -35
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +20 -1
- data/lib/tasks/apartment.rake +41 -14
- data/ros-apartment.gemspec +2 -2
- metadata +10 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af86a04df8bf00d04859470cd76a8228d6b984da90e1324ed9eff43cbd0db0ac
|
|
4
|
+
data.tar.gz: 905caaf498b70015ddccc9d8846ec18384fac0a98f6dd1010890a8fa2fb19e97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dfea9df160a8eb0e400f215eada4ecfb41852ab4d92123e1ee51bf2f1a25069c24a36a6b471048d70091a03af5efd420af6657e6bba38748630f285a5aa344cf
|
|
7
|
+
data.tar.gz: 9a521c51af92800ac7b42a2f7f7ef48322d075095aed7868807caecc248958fc0667e3c0ae22b14e17d9df44ada1c325ca9804e03bdc9947a2f90bcf1a6bb27a
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.7
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Repository Guidelines for AI Agents
|
|
2
|
+
|
|
3
|
+
This repo uses `CLAUDE.md` files as authoritative contributor guides for all AI assistants.
|
|
4
|
+
|
|
5
|
+
## Reading Order
|
|
6
|
+
|
|
7
|
+
1. **Read `/CLAUDE.md` (root) first** - project-wide architecture, patterns, design decisions
|
|
8
|
+
2. **Read directory-specific `CLAUDE.md`** - check the directory you're modifying and its parents
|
|
9
|
+
3. **Nested files override root** on conflicts (per AGENTS.md spec)
|
|
10
|
+
|
|
11
|
+
Nested `CLAUDE.md` files exist in `lib/apartment/`, `lib/apartment/adapters/`, `lib/apartment/elevators/`, `lib/apartment/tasks/`, and `spec/`.
|
|
12
|
+
|
|
13
|
+
## Key Principle
|
|
14
|
+
|
|
15
|
+
Check `CLAUDE.md` before copying patterns from existing code - it documents preferred patterns, design rationale, and known pitfalls.
|
|
16
|
+
|
|
17
|
+
## Adding Documentation
|
|
18
|
+
|
|
19
|
+
Update the appropriate `CLAUDE.md` rather than this file. This file exists only as a pointer.
|
data/README.md
CHANGED
|
@@ -23,6 +23,12 @@ As of May 2024, Apartment is maintained with the support of [CampusESP](https://
|
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
26
|
+
### Requirements
|
|
27
|
+
|
|
28
|
+
- Ruby 3.1+
|
|
29
|
+
- Rails 7.0+ (Rails 6.1 support was dropped in v3.4.0)
|
|
30
|
+
- PostgreSQL, MySQL, or SQLite3
|
|
31
|
+
|
|
26
32
|
### Rails
|
|
27
33
|
|
|
28
34
|
Add the following to your Gemfile:
|
|
@@ -531,14 +537,55 @@ Note that you can disable the default migrating of all tenants with `db:migrate`
|
|
|
531
537
|
|
|
532
538
|
#### Parallel Migrations
|
|
533
539
|
|
|
534
|
-
Apartment supports
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
540
|
+
Apartment supports parallel tenant migrations for applications with many schemas where sequential migration time becomes problematic. This is an **advanced feature** that requires understanding of your migration safety guarantees.
|
|
541
|
+
|
|
542
|
+
##### Enabling Parallel Migrations
|
|
543
|
+
|
|
544
|
+
```ruby
|
|
545
|
+
Apartment.configure do |config|
|
|
546
|
+
config.parallel_migration_threads = 4
|
|
547
|
+
end
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
##### Configuration Options
|
|
551
|
+
|
|
552
|
+
| Option | Default | Description |
|
|
553
|
+
|--------|---------|-------------|
|
|
554
|
+
| `parallel_migration_threads` | `0` | Number of parallel workers. `0` disables parallelism (recommended default). |
|
|
555
|
+
| `parallel_strategy` | `:auto` | `:auto` detects platform, `:threads` forces thread-based, `:processes` forces fork-based. |
|
|
556
|
+
| `manage_advisory_locks` | `true` | Disables PostgreSQL advisory locks during parallel execution to prevent deadlocks. |
|
|
557
|
+
|
|
558
|
+
##### Platform Considerations
|
|
559
|
+
|
|
560
|
+
Apartment auto-detects the safest parallelism strategy for your platform:
|
|
561
|
+
|
|
562
|
+
- **Linux**: Uses process-based parallelism (faster due to copy-on-write memory)
|
|
563
|
+
- **macOS/Windows**: Uses thread-based parallelism (avoids libpq fork issues)
|
|
564
|
+
|
|
565
|
+
You can override this with `parallel_strategy: :threads` or `parallel_strategy: :processes`, but forcing processes on macOS may cause crashes due to PostgreSQL's C library (libpq) not being fork-safe on that platform.
|
|
566
|
+
|
|
567
|
+
##### Important: Your Responsibility
|
|
568
|
+
|
|
569
|
+
When you enable parallel migrations, Apartment disables PostgreSQL advisory locks to prevent deadlocks. This means **you are responsible for ensuring your migrations are safe to run concurrently**.
|
|
570
|
+
|
|
571
|
+
**Use parallel migrations when:**
|
|
572
|
+
- You have many tenants and sequential migration time is problematic
|
|
573
|
+
- Your migrations only modify objects within each tenant's schema
|
|
574
|
+
- You've verified your migrations have no cross-schema side effects
|
|
575
|
+
|
|
576
|
+
**Stick with sequential execution when:**
|
|
577
|
+
- Migrations create or modify PostgreSQL extensions
|
|
578
|
+
- Migrations modify shared types, functions, or other database-wide objects
|
|
579
|
+
- Migrations have ordering dependencies that span tenants
|
|
580
|
+
- You're unsure whether your migrations are parallel-safe
|
|
581
|
+
|
|
582
|
+
##### Connection Pool Sizing
|
|
583
|
+
|
|
584
|
+
The `parallel_migration_threads` value should be less than your database connection pool size to avoid exhaustion errors. If you set `parallel_migration_threads: 8`, ensure your `pool` setting in `database.yml` is at least 10 to leave headroom.
|
|
585
|
+
|
|
586
|
+
##### Schema Dump After Migration
|
|
538
587
|
|
|
539
|
-
|
|
540
|
-
the number of threads indicated here should be less than the pool size
|
|
541
|
-
that Rails will use to connect to your database.
|
|
588
|
+
Apartment automatically dumps `schema.rb` after successful migrations, ensuring the dump comes from the public schema (the source of truth). This respects Rails' `dump_schema_after_migration` setting.
|
|
542
589
|
|
|
543
590
|
### Handling Environments
|
|
544
591
|
|
data/lib/apartment/CLAUDE.md
CHANGED
|
@@ -6,11 +6,11 @@ This directory contains the core implementation of Apartment v3's multi-tenancy
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
lib/apartment/
|
|
9
|
-
├── adapters/ # Database-specific tenant isolation strategies
|
|
9
|
+
├── adapters/ # Database-specific tenant isolation strategies (see CLAUDE.md)
|
|
10
10
|
├── active_record/ # ActiveRecord patches and extensions
|
|
11
|
-
├── elevators/ # Rack middleware for automatic tenant switching
|
|
11
|
+
├── elevators/ # Rack middleware for automatic tenant switching (see CLAUDE.md)
|
|
12
12
|
├── patches/ # Ruby/Rails core patches
|
|
13
|
-
├── tasks/ # Rake task utilities
|
|
13
|
+
├── tasks/ # Rake task utilities, parallel migrations (see CLAUDE.md)
|
|
14
14
|
├── console.rb # Rails console tenant switching utilities
|
|
15
15
|
├── custom_console.rb # Enhanced console with tenant prompts
|
|
16
16
|
├── deprecation.rb # Deprecation warnings configuration
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# lib/apartment/tasks/ - Rake Task Infrastructure
|
|
2
|
+
|
|
3
|
+
This directory contains modules that support Apartment's rake task operations, particularly tenant migrations with optional parallelism.
|
|
4
|
+
|
|
5
|
+
## Problem Context
|
|
6
|
+
|
|
7
|
+
Multi-tenant PostgreSQL applications using schema-per-tenant isolation face operational challenges:
|
|
8
|
+
|
|
9
|
+
1. **Migration time scales linearly**: 100 tenants × 2 seconds each = 3+ minutes blocking deploys
|
|
10
|
+
2. **Rails assumes single-schema**: Built-in migration tasks don't iterate over tenant schemas
|
|
11
|
+
3. **Parallel execution has pitfalls**: Database connections, advisory locks, and platform differences create subtle failure modes
|
|
12
|
+
|
|
13
|
+
## Files
|
|
14
|
+
|
|
15
|
+
### task_helper.rb
|
|
16
|
+
|
|
17
|
+
**Purpose**: Orchestrates tenant iteration for rake tasks with optional parallel execution.
|
|
18
|
+
|
|
19
|
+
**Key decisions**:
|
|
20
|
+
|
|
21
|
+
- **Result-based error handling**: Operations return `Result` structs instead of raising exceptions. This allows migrations to continue for other tenants when one fails, with aggregated reporting at the end.
|
|
22
|
+
|
|
23
|
+
- **Platform-aware parallelism**: macOS has documented issues with libpq after `fork()` due to GSS/Kerberos state. We auto-detect the platform and choose threads (safe everywhere) or processes (faster on Linux) accordingly. Developers can override via `parallel_strategy` config.
|
|
24
|
+
|
|
25
|
+
- **Advisory lock management**: Rails uses `pg_advisory_lock` to prevent concurrent migrations. With parallel tenant migrations, all workers compete for the same lock, causing deadlocks. We disable advisory locks during parallel execution. **This shifts responsibility to the developer** to ensure migrations are parallel-safe.
|
|
26
|
+
|
|
27
|
+
**When to use parallel migrations**:
|
|
28
|
+
|
|
29
|
+
Use when you have many tenants and your migrations only touch tenant-specific objects. Avoid when migrations create extensions, modify shared types, or have cross-tenant dependencies.
|
|
30
|
+
|
|
31
|
+
**Configuration options** (set in `config/initializers/apartment.rb`):
|
|
32
|
+
|
|
33
|
+
| Option | Default | Purpose |
|
|
34
|
+
|--------|---------|---------|
|
|
35
|
+
| `parallel_migration_threads` | `0` | Worker count. 0 = sequential (safest) |
|
|
36
|
+
| `parallel_strategy` | `:auto` | `:auto`, `:threads`, or `:processes` |
|
|
37
|
+
| `manage_advisory_locks` | `true` | Disable locks during parallel execution |
|
|
38
|
+
|
|
39
|
+
### schema_dumper.rb
|
|
40
|
+
|
|
41
|
+
**Purpose**: Ensures schema is dumped from the public schema after tenant migrations.
|
|
42
|
+
|
|
43
|
+
**Why this exists**: After `rails db:migrate`, Rails dumps the current schema. Without intervention, this could capture the last-migrated tenant's schema rather than the authoritative public schema. We switch to the default tenant before invoking the dump.
|
|
44
|
+
|
|
45
|
+
**Rails convention compliance**: Respects all relevant Rails settings:
|
|
46
|
+
- `dump_schema_after_migration`: Global toggle for automatic dumps
|
|
47
|
+
- `schema_format`: `:ruby` produces schema.rb, `:sql` produces structure.sql
|
|
48
|
+
- `database_tasks`, `replica`, `schema_dump`: Per-database settings
|
|
49
|
+
|
|
50
|
+
### enhancements.rb
|
|
51
|
+
|
|
52
|
+
**Purpose**: Hooks Apartment tasks into Rails' standard `db:migrate` and `db:rollback` tasks.
|
|
53
|
+
|
|
54
|
+
**Design choice**: We enhance rather than replace Rails tasks. Running `rails db:migrate` automatically migrates all tenant schemas after the public schema.
|
|
55
|
+
|
|
56
|
+
## Relationship to Other Components
|
|
57
|
+
|
|
58
|
+
- **Apartment::Migrator** (`lib/apartment/migrator.rb`): The actual migration execution logic. TaskHelper coordinates which tenants to migrate; Migrator handles the per-tenant work.
|
|
59
|
+
|
|
60
|
+
- **Rake tasks** (`lib/tasks/apartment.rake`): Define the public task interface (`apartment:migrate`, etc.). These tasks use TaskHelper for iteration.
|
|
61
|
+
|
|
62
|
+
- **Configuration** (`lib/apartment.rb`): Parallel execution settings live in the main Apartment module.
|
|
63
|
+
|
|
64
|
+
## Common Failure Modes
|
|
65
|
+
|
|
66
|
+
### Connection pool exhaustion
|
|
67
|
+
|
|
68
|
+
**Symptom**: "could not obtain a connection from the pool" errors
|
|
69
|
+
|
|
70
|
+
**Cause**: `parallel_migration_threads` exceeds database pool size
|
|
71
|
+
|
|
72
|
+
**Fix**: Ensure `pool` in `database.yml` > `parallel_migration_threads`
|
|
73
|
+
|
|
74
|
+
### Advisory lock deadlocks
|
|
75
|
+
|
|
76
|
+
**Symptom**: Migrations hang indefinitely
|
|
77
|
+
|
|
78
|
+
**Cause**: Multiple workers waiting for the same advisory lock
|
|
79
|
+
|
|
80
|
+
**Fix**: Ensure `manage_advisory_locks: true` (default) when using parallelism
|
|
81
|
+
|
|
82
|
+
### macOS fork crashes
|
|
83
|
+
|
|
84
|
+
**Symptom**: Segfaults or GSS-API errors when using process-based parallelism on macOS
|
|
85
|
+
|
|
86
|
+
**Cause**: libpq doesn't support fork() cleanly on macOS
|
|
87
|
+
|
|
88
|
+
**Fix**: Use `parallel_strategy: :threads` or rely on `:auto` detection
|
|
89
|
+
|
|
90
|
+
### Empty tenant name errors
|
|
91
|
+
|
|
92
|
+
**Symptom**: `PG::SyntaxError: zero-length delimited identifier`
|
|
93
|
+
|
|
94
|
+
**Cause**: `tenant_names` proc returned empty strings or nil values
|
|
95
|
+
|
|
96
|
+
**Fix**: Fixed in v3.4.0 - empty values are now filtered automatically
|
|
97
|
+
|
|
98
|
+
## Testing Considerations
|
|
99
|
+
|
|
100
|
+
Parallel execution paths are difficult to unit test due to process isolation and connection state. The test suite verifies:
|
|
101
|
+
|
|
102
|
+
- Correct delegation between sequential/parallel paths
|
|
103
|
+
- Platform detection logic
|
|
104
|
+
- Advisory lock ENV management
|
|
105
|
+
- Result aggregation and error capture
|
|
106
|
+
|
|
107
|
+
Integration testing of actual parallel execution happens in CI across Linux (processes) and macOS (threads) runners.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apartment
|
|
4
|
+
module Tasks
|
|
5
|
+
# Handles automatic schema dumping after tenant migrations.
|
|
6
|
+
#
|
|
7
|
+
# ## Problem Context
|
|
8
|
+
#
|
|
9
|
+
# After running `rails db:migrate`, Rails dumps the schema to capture the
|
|
10
|
+
# current database structure. With Apartment, tenant migrations modify
|
|
11
|
+
# individual schemas but the canonical structure lives in the public/default
|
|
12
|
+
# schema. Without explicit handling, the schema could be dumped from the
|
|
13
|
+
# last-migrated tenant schema instead of the authoritative public schema.
|
|
14
|
+
#
|
|
15
|
+
# ## Why This Approach
|
|
16
|
+
#
|
|
17
|
+
# We switch to the default tenant before dumping to ensure the schema file
|
|
18
|
+
# reflects the public schema structure. This is correct because:
|
|
19
|
+
#
|
|
20
|
+
# 1. All tenant schemas are created from the same schema file
|
|
21
|
+
# 2. The public schema is the source of truth for structure
|
|
22
|
+
# 3. Tenant-specific data differences don't affect schema structure
|
|
23
|
+
#
|
|
24
|
+
# ## Rails Convention Compliance
|
|
25
|
+
#
|
|
26
|
+
# We respect several Rails configurations rather than inventing our own:
|
|
27
|
+
#
|
|
28
|
+
# - `config.active_record.dump_schema_after_migration`: Global toggle
|
|
29
|
+
# - `config.active_record.schema_format`: `:ruby` for schema.rb, `:sql` for structure.sql
|
|
30
|
+
# - `database_tasks: true/false`: Per-database migration responsibility
|
|
31
|
+
# - `replica: true`: Excludes read replicas from schema operations
|
|
32
|
+
# - `schema_dump: false`: Per-database schema dump toggle
|
|
33
|
+
#
|
|
34
|
+
# The `db:schema:dump` task respects `schema_format` and produces either
|
|
35
|
+
# schema.rb or structure.sql accordingly.
|
|
36
|
+
#
|
|
37
|
+
# ## Gotchas
|
|
38
|
+
#
|
|
39
|
+
# - Schema dump failures are logged but don't fail the migration. This
|
|
40
|
+
# prevents a secondary concern from blocking critical migrations.
|
|
41
|
+
# - Multi-database setups must mark one connection with `database_tasks: true`
|
|
42
|
+
# to indicate which database owns schema management.
|
|
43
|
+
# - Don't call `Rails.application.load_tasks` here; if invoked from a rake
|
|
44
|
+
# task, it re-triggers apartment enhancements causing recursion.
|
|
45
|
+
module SchemaDumper
|
|
46
|
+
class << self
|
|
47
|
+
# Entry point called after successful migrations. Checks all relevant
|
|
48
|
+
# Rails settings before attempting dump.
|
|
49
|
+
def dump_if_enabled
|
|
50
|
+
return unless rails_dump_schema_enabled?
|
|
51
|
+
|
|
52
|
+
db_config = find_schema_dump_config
|
|
53
|
+
return if db_config.nil?
|
|
54
|
+
|
|
55
|
+
schema_dump_setting = db_config.configuration_hash[:schema_dump]
|
|
56
|
+
return if schema_dump_setting == false
|
|
57
|
+
|
|
58
|
+
Apartment::Tenant.switch(Apartment.default_tenant) do
|
|
59
|
+
dump_schema
|
|
60
|
+
end
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
# Log but don't fail - schema dump is secondary to migration success
|
|
63
|
+
Rails.logger.warn("[Apartment] Schema dump failed: #{e.message}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Finds the database configuration responsible for schema management.
|
|
69
|
+
# Multi-database setups use `database_tasks: true` to mark the primary
|
|
70
|
+
# migration database. Falls back to 'primary' named config.
|
|
71
|
+
def find_schema_dump_config
|
|
72
|
+
configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
|
|
73
|
+
|
|
74
|
+
migration_config = configs.find { |c| c.database_tasks? && !c.replica? }
|
|
75
|
+
return migration_config if migration_config
|
|
76
|
+
|
|
77
|
+
configs.find { |c| c.name == 'primary' }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Invokes the standard Rails schema dump task. We reenable first
|
|
81
|
+
# because Rake tasks can only run once per session by default.
|
|
82
|
+
def dump_schema
|
|
83
|
+
if task_defined?('db:schema:dump')
|
|
84
|
+
Rails.logger.info('[Apartment] Dumping schema from default tenant...')
|
|
85
|
+
Rake::Task['db:schema:dump'].reenable
|
|
86
|
+
Rake::Task['db:schema:dump'].invoke
|
|
87
|
+
Rails.logger.info('[Apartment] Schema dump completed.')
|
|
88
|
+
else
|
|
89
|
+
Rails.logger.warn('[Apartment] db:schema:dump task not found')
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Safe task existence check. Avoids load_tasks which would cause
|
|
94
|
+
# recursive enhancement loading when called from apartment rake tasks.
|
|
95
|
+
def task_defined?(task_name)
|
|
96
|
+
Rake::Task.task_defined?(task_name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Checks Rails' global schema dump setting. Older Rails versions
|
|
100
|
+
# may not have this method, so we default to enabled.
|
|
101
|
+
def rails_dump_schema_enabled?
|
|
102
|
+
return true unless ActiveRecord::Base.respond_to?(:dump_schema_after_migration)
|
|
103
|
+
|
|
104
|
+
ActiveRecord::Base.dump_schema_after_migration
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -1,54 +1,292 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
|
4
|
+
|
|
3
5
|
module Apartment
|
|
6
|
+
# Coordinates tenant operations for rake tasks with parallel execution support.
|
|
7
|
+
#
|
|
8
|
+
# ## Problem Context
|
|
9
|
+
#
|
|
10
|
+
# Multi-tenant applications with many schemas face slow migration times when
|
|
11
|
+
# running sequentially. A 100-tenant system with 2-second migrations takes
|
|
12
|
+
# 3+ minutes sequentially but ~20 seconds with 10 parallel workers.
|
|
13
|
+
#
|
|
14
|
+
# ## Why This Design
|
|
15
|
+
#
|
|
16
|
+
# Parallel database migrations introduce two categories of problems:
|
|
17
|
+
#
|
|
18
|
+
# 1. **Platform-specific fork safety**: macOS/Windows have issues with libpq
|
|
19
|
+
# (PostgreSQL C library) after fork() due to GSS/Kerberos state corruption.
|
|
20
|
+
# Linux handles fork() cleanly. We auto-detect and choose the safe strategy.
|
|
21
|
+
#
|
|
22
|
+
# 2. **PostgreSQL advisory lock deadlocks**: Rails uses advisory locks to
|
|
23
|
+
# prevent concurrent migrations. When multiple processes/threads migrate
|
|
24
|
+
# different schemas simultaneously, they deadlock competing for the same
|
|
25
|
+
# lock. We disable advisory locks during parallel execution, which means
|
|
26
|
+
# **you accept responsibility for ensuring your migrations are parallel-safe**.
|
|
27
|
+
#
|
|
28
|
+
# ## When to Use Parallel Migrations
|
|
29
|
+
#
|
|
30
|
+
# This is an advanced feature. Use it when:
|
|
31
|
+
# - You have many tenants and sequential migration time is problematic
|
|
32
|
+
# - Your migrations only modify tenant-specific schema objects
|
|
33
|
+
# - You've verified your migrations don't have cross-schema side effects
|
|
34
|
+
#
|
|
35
|
+
# Stick with sequential execution (the default) when:
|
|
36
|
+
# - Migrations create/modify extensions, types, or shared objects
|
|
37
|
+
# - Migrations have ordering dependencies across tenants
|
|
38
|
+
# - You're unsure whether parallel execution is safe for your use case
|
|
39
|
+
#
|
|
40
|
+
# ## Gotchas
|
|
41
|
+
#
|
|
42
|
+
# - The `parallel_migration_threads` count should be less than your connection
|
|
43
|
+
# pool size to avoid connection exhaustion.
|
|
44
|
+
# - Empty/nil tenant names from `tenant_names` proc are filtered to prevent
|
|
45
|
+
# PostgreSQL "zero-length delimited identifier" errors.
|
|
46
|
+
# - Process-based parallelism requires fresh connections in each fork;
|
|
47
|
+
# thread-based parallelism shares the pool but needs explicit checkout.
|
|
48
|
+
#
|
|
49
|
+
# @see Apartment.parallel_migration_threads
|
|
50
|
+
# @see Apartment.parallel_strategy
|
|
51
|
+
# @see Apartment.manage_advisory_locks
|
|
4
52
|
module TaskHelper
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
53
|
+
# Captures outcome per tenant for aggregated reporting. Allows migrations
|
|
54
|
+
# to continue for remaining tenants even when one fails.
|
|
55
|
+
Result = Struct.new(:tenant, :success, :error, keyword_init: true)
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# Primary entry point for tenant iteration. Automatically selects
|
|
59
|
+
# sequential or parallel execution based on configuration.
|
|
60
|
+
#
|
|
61
|
+
# @yield [String] tenant name
|
|
62
|
+
# @return [Array<Result>] outcome for each tenant
|
|
63
|
+
def each_tenant(&)
|
|
64
|
+
return [] if tenants_without_default.empty?
|
|
65
|
+
|
|
66
|
+
if parallel_migration_threads.positive?
|
|
67
|
+
each_tenant_parallel(&)
|
|
68
|
+
else
|
|
69
|
+
each_tenant_sequential(&)
|
|
9
70
|
end
|
|
10
71
|
end
|
|
11
|
-
end
|
|
12
72
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
73
|
+
# Sequential execution: simpler, no connection management complexity.
|
|
74
|
+
# Used when parallel_migration_threads is 0 (the default).
|
|
75
|
+
def each_tenant_sequential
|
|
76
|
+
tenants_without_default.map do |tenant|
|
|
77
|
+
Rails.application.executor.wrap do
|
|
78
|
+
yield(tenant)
|
|
79
|
+
end
|
|
80
|
+
Result.new(tenant: tenant, success: true, error: nil)
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
Result.new(tenant: tenant, success: false, error: e.message)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
16
85
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
86
|
+
# Parallel execution wrapper. Disables advisory locks for the duration,
|
|
87
|
+
# then delegates to platform-appropriate parallelism strategy.
|
|
88
|
+
def each_tenant_parallel(&)
|
|
89
|
+
with_advisory_locks_disabled do
|
|
90
|
+
case resolve_parallel_strategy
|
|
91
|
+
when :processes
|
|
92
|
+
each_tenant_in_processes(&)
|
|
93
|
+
else
|
|
94
|
+
each_tenant_in_threads(&)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
20
98
|
|
|
21
|
-
|
|
22
|
-
|
|
99
|
+
# Process-based parallelism via fork(). Faster on Linux due to
|
|
100
|
+
# copy-on-write memory and no GIL contention. Each forked process
|
|
101
|
+
# gets isolated memory, so we must clear inherited connections
|
|
102
|
+
# and establish fresh ones.
|
|
103
|
+
def each_tenant_in_processes
|
|
104
|
+
Parallel.map(tenants_without_default, in_processes: parallel_migration_threads) do |tenant|
|
|
105
|
+
# Forked processes inherit parent's connection handles but the
|
|
106
|
+
# underlying sockets are invalid. Must reconnect before any DB work.
|
|
107
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
108
|
+
reconnect_for_parallel_execution
|
|
23
109
|
|
|
24
|
-
|
|
25
|
-
|
|
110
|
+
Rails.application.executor.wrap do
|
|
111
|
+
yield(tenant)
|
|
112
|
+
end
|
|
113
|
+
Result.new(tenant: tenant, success: true, error: nil)
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
Result.new(tenant: tenant, success: false, error: e.message)
|
|
116
|
+
ensure
|
|
117
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
26
120
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
121
|
+
# Thread-based parallelism. Safe on all platforms but subject to GIL
|
|
122
|
+
# for CPU-bound work (migrations are typically I/O-bound, so this is fine).
|
|
123
|
+
# Threads share the connection pool, so we reconfigure once before
|
|
124
|
+
# spawning and restore after completion.
|
|
125
|
+
def each_tenant_in_threads
|
|
126
|
+
original_config = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
127
|
+
reconnect_for_parallel_execution
|
|
30
128
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
129
|
+
Parallel.map(tenants_without_default, in_threads: parallel_migration_threads) do |tenant|
|
|
130
|
+
# Explicit connection checkout prevents pool exhaustion when
|
|
131
|
+
# thread count exceeds pool size minus buffer.
|
|
132
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
|
133
|
+
Rails.application.executor.wrap do
|
|
134
|
+
yield(tenant)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
Result.new(tenant: tenant, success: true, error: nil)
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
Result.new(tenant: tenant, success: false, error: e.message)
|
|
140
|
+
end
|
|
141
|
+
ensure
|
|
142
|
+
ActiveRecord::Base.connection_handler.clear_all_connections!(:all)
|
|
143
|
+
ActiveRecord::Base.establish_connection(original_config) if original_config
|
|
144
|
+
end
|
|
34
145
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
146
|
+
# Auto-detection logic for parallelism strategy. Only Linux gets
|
|
147
|
+
# process-based parallelism by default due to macOS libpq fork issues.
|
|
148
|
+
def resolve_parallel_strategy
|
|
149
|
+
strategy = Apartment.parallel_strategy
|
|
150
|
+
|
|
151
|
+
return :threads if strategy == :threads
|
|
152
|
+
return :processes if strategy == :processes
|
|
153
|
+
|
|
154
|
+
fork_safe_platform? ? :processes : :threads
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Platform detection. Conservative: only Linux is considered fork-safe.
|
|
158
|
+
# macOS has documented issues with libpq, GSS-API, and Kerberos after fork.
|
|
159
|
+
# See: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-GSSENCMODE
|
|
160
|
+
def fork_safe_platform?
|
|
161
|
+
RUBY_PLATFORM.include?('linux')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Advisory lock management. Rails acquires pg_advisory_lock during migrations
|
|
165
|
+
# to prevent concurrent schema changes. With parallel tenant migrations,
|
|
166
|
+
# this causes deadlocks since all workers compete for the same lock.
|
|
167
|
+
#
|
|
168
|
+
# **Important**: Disabling advisory locks shifts responsibility to you.
|
|
169
|
+
# Your migrations must be safe to run concurrently across tenants. If your
|
|
170
|
+
# migrations modify shared resources, create extensions, or have other
|
|
171
|
+
# cross-schema side effects, parallel execution may cause failures.
|
|
172
|
+
# When in doubt, use sequential execution (parallel_migration_threads = 0).
|
|
173
|
+
#
|
|
174
|
+
# Uses ENV var because Rails checks it at connection establishment time,
|
|
175
|
+
# and we need it disabled before Parallel spawns workers.
|
|
176
|
+
def with_advisory_locks_disabled
|
|
177
|
+
return yield unless parallel_migration_threads.positive?
|
|
178
|
+
return yield unless Apartment.manage_advisory_locks
|
|
179
|
+
|
|
180
|
+
original_env_value = ENV.fetch('DISABLE_ADVISORY_LOCKS', nil)
|
|
181
|
+
begin
|
|
182
|
+
ENV['DISABLE_ADVISORY_LOCKS'] = 'true'
|
|
183
|
+
yield
|
|
184
|
+
ensure
|
|
185
|
+
if original_env_value.nil?
|
|
186
|
+
ENV.delete('DISABLE_ADVISORY_LOCKS')
|
|
187
|
+
else
|
|
188
|
+
ENV['DISABLE_ADVISORY_LOCKS'] = original_env_value
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
41
192
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
193
|
+
# Re-establishes database connection for parallel execution.
|
|
194
|
+
# When manage_advisory_locks is true, disables advisory locks in the
|
|
195
|
+
# connection config (belt-and-suspenders with the ENV var approach).
|
|
196
|
+
# When false, reconnects with existing config unchanged.
|
|
197
|
+
def reconnect_for_parallel_execution
|
|
198
|
+
current_config = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
45
199
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
200
|
+
new_config = if Apartment.manage_advisory_locks
|
|
201
|
+
current_config.merge(advisory_locks: false)
|
|
202
|
+
else
|
|
203
|
+
current_config
|
|
204
|
+
end
|
|
50
205
|
|
|
51
|
-
|
|
206
|
+
ActiveRecord::Base.establish_connection(new_config)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Delegate to Apartment.parallel_migration_threads
|
|
210
|
+
delegate :parallel_migration_threads, to: Apartment
|
|
211
|
+
|
|
212
|
+
# Get list of tenants excluding the default tenant
|
|
213
|
+
# Also filters out blank/empty tenant names to prevent errors
|
|
214
|
+
#
|
|
215
|
+
# @return [Array<String>] tenant names
|
|
216
|
+
def tenants_without_default
|
|
217
|
+
(tenants - [Apartment.default_tenant]).reject { |t| t.nil? || t.to_s.strip.empty? }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get list of all tenants to operate on
|
|
221
|
+
# Supports DB env var for targeting specific tenants
|
|
222
|
+
# Filters out blank tenant names for safety
|
|
223
|
+
#
|
|
224
|
+
# @return [Array<String>] tenant names
|
|
225
|
+
def tenants
|
|
226
|
+
result = ENV['DB'] ? ENV['DB'].split(',').map(&:strip) : Apartment.tenant_names || []
|
|
227
|
+
result.reject { |t| t.nil? || t.to_s.strip.empty? }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Display warning if tenant list is empty
|
|
231
|
+
def warn_if_tenants_empty
|
|
232
|
+
return unless tenants.empty? && ENV['IGNORE_EMPTY_TENANTS'] != 'true'
|
|
233
|
+
|
|
234
|
+
puts <<~WARNING
|
|
235
|
+
[WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things:
|
|
236
|
+
|
|
237
|
+
1. You may not have created any, in which case you can ignore this message
|
|
238
|
+
2. You've run `apartment:migrate` directly without loading the Rails environment
|
|
239
|
+
* `apartment:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate`
|
|
240
|
+
|
|
241
|
+
Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this.
|
|
242
|
+
WARNING
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Display summary of operation results
|
|
246
|
+
#
|
|
247
|
+
# @param operation [String] name of the operation (e.g., "Migration", "Rollback")
|
|
248
|
+
# @param results [Array<Result>] results from each_tenant
|
|
249
|
+
def display_summary(operation, results)
|
|
250
|
+
return if results.empty?
|
|
251
|
+
|
|
252
|
+
succeeded = results.count(&:success)
|
|
253
|
+
failed = results.reject(&:success)
|
|
254
|
+
|
|
255
|
+
puts "\n=== #{operation} Summary ==="
|
|
256
|
+
puts "Succeeded: #{succeeded}/#{results.size} tenants"
|
|
257
|
+
|
|
258
|
+
return if failed.empty?
|
|
259
|
+
|
|
260
|
+
puts "Failed: #{failed.size} tenants"
|
|
261
|
+
failed.each do |result|
|
|
262
|
+
puts " - #{result.tenant}: #{result.error}"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Create a tenant with logging
|
|
267
|
+
#
|
|
268
|
+
# @param tenant_name [String] name of tenant to create
|
|
269
|
+
def create_tenant(tenant_name)
|
|
270
|
+
puts("Creating #{tenant_name} tenant")
|
|
271
|
+
Apartment::Tenant.create(tenant_name)
|
|
272
|
+
rescue Apartment::TenantExists => e
|
|
273
|
+
puts "Tried to create already existing tenant: #{e}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Migrate a single tenant with error handling based on strategy
|
|
277
|
+
#
|
|
278
|
+
# @param tenant_name [String] name of tenant to migrate
|
|
279
|
+
def migrate_tenant(tenant_name)
|
|
280
|
+
strategy = Apartment.db_migrate_tenant_missing_strategy
|
|
281
|
+
create_tenant(tenant_name) if strategy == :create_tenant
|
|
282
|
+
|
|
283
|
+
puts("Migrating #{tenant_name} tenant")
|
|
284
|
+
Apartment::Migrator.migrate(tenant_name)
|
|
285
|
+
rescue Apartment::TenantNotFound => e
|
|
286
|
+
raise(e) if strategy == :raise_exception
|
|
287
|
+
|
|
288
|
+
puts e.message
|
|
289
|
+
end
|
|
52
290
|
end
|
|
53
291
|
end
|
|
54
292
|
end
|
data/lib/apartment/version.rb
CHANGED
data/lib/apartment.rb
CHANGED
|
@@ -28,7 +28,8 @@ module Apartment
|
|
|
28
28
|
WRITER_METHODS = %i[tenant_names database_schema_file excluded_models
|
|
29
29
|
persistent_schemas connection_class
|
|
30
30
|
db_migrate_tenants db_migrate_tenant_missing_strategy seed_data_file
|
|
31
|
-
parallel_migration_threads pg_excluded_names
|
|
31
|
+
parallel_migration_threads pg_excluded_names
|
|
32
|
+
parallel_strategy manage_advisory_locks].freeze
|
|
32
33
|
|
|
33
34
|
attr_accessor(*ACCESSOR_METHODS)
|
|
34
35
|
attr_writer(*WRITER_METHODS)
|
|
@@ -93,6 +94,24 @@ module Apartment
|
|
|
93
94
|
@parallel_migration_threads || 0
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
# Parallelism strategy for migrations
|
|
98
|
+
# :auto (default) - Detect platform: processes on Linux, threads on macOS/Windows
|
|
99
|
+
# :threads - Always use threads (safer, works everywhere)
|
|
100
|
+
# :processes - Always use processes (faster on Linux, may crash on macOS/Windows)
|
|
101
|
+
def parallel_strategy
|
|
102
|
+
@parallel_strategy || :auto
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Whether to manage PostgreSQL advisory locks during parallel migrations
|
|
106
|
+
# When true and parallel_migration_threads > 0, advisory locks are disabled
|
|
107
|
+
# during migration to prevent deadlocks, then restored afterward.
|
|
108
|
+
# Default: true
|
|
109
|
+
def manage_advisory_locks
|
|
110
|
+
return @manage_advisory_locks if defined?(@manage_advisory_locks)
|
|
111
|
+
|
|
112
|
+
@manage_advisory_locks = true
|
|
113
|
+
end
|
|
114
|
+
|
|
96
115
|
def persistent_schemas
|
|
97
116
|
@persistent_schemas || []
|
|
98
117
|
end
|
data/lib/tasks/apartment.rake
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'apartment/migrator'
|
|
4
4
|
require 'apartment/tasks/task_helper'
|
|
5
|
+
require 'apartment/tasks/schema_dumper'
|
|
5
6
|
require 'parallel'
|
|
6
7
|
|
|
7
8
|
apartment_namespace = namespace(:apartment) do
|
|
@@ -27,9 +28,22 @@ apartment_namespace = namespace(:apartment) do
|
|
|
27
28
|
desc('Migrate all tenants')
|
|
28
29
|
task(migrate: :environment) do
|
|
29
30
|
Apartment::TaskHelper.warn_if_tenants_empty
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
results = Apartment::TaskHelper.each_tenant do |tenant|
|
|
31
33
|
Apartment::TaskHelper.migrate_tenant(tenant)
|
|
32
34
|
end
|
|
35
|
+
|
|
36
|
+
Apartment::TaskHelper.display_summary('Migration', results)
|
|
37
|
+
|
|
38
|
+
# Dump schema after successful migrations
|
|
39
|
+
if results.all?(&:success)
|
|
40
|
+
Apartment::Tasks::SchemaDumper.dump_if_enabled
|
|
41
|
+
else
|
|
42
|
+
puts '[Apartment] Skipping schema dump due to migration failures'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Exit with non-zero status if any tenant failed
|
|
46
|
+
exit(1) if results.any? { |r| !r.success }
|
|
33
47
|
end
|
|
34
48
|
|
|
35
49
|
desc('Seed all tenants')
|
|
@@ -51,14 +65,23 @@ apartment_namespace = namespace(:apartment) do
|
|
|
51
65
|
task(rollback: :environment) do
|
|
52
66
|
Apartment::TaskHelper.warn_if_tenants_empty
|
|
53
67
|
|
|
54
|
-
step = ENV
|
|
68
|
+
step = ENV.fetch('STEP', '1').to_i
|
|
55
69
|
|
|
56
|
-
Apartment::TaskHelper.each_tenant do |tenant|
|
|
70
|
+
results = Apartment::TaskHelper.each_tenant do |tenant|
|
|
57
71
|
puts("Rolling back #{tenant} tenant")
|
|
58
72
|
Apartment::Migrator.rollback(tenant, step)
|
|
59
|
-
rescue Apartment::TenantNotFound => e
|
|
60
|
-
puts e.message
|
|
61
73
|
end
|
|
74
|
+
|
|
75
|
+
Apartment::TaskHelper.display_summary('Rollback', results)
|
|
76
|
+
|
|
77
|
+
# Dump schema after successful rollback
|
|
78
|
+
if results.all?(&:success)
|
|
79
|
+
Apartment::Tasks::SchemaDumper.dump_if_enabled
|
|
80
|
+
else
|
|
81
|
+
puts '[Apartment] Skipping schema dump due to rollback failures'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
exit(1) if results.any? { |r| !r.success }
|
|
62
85
|
end
|
|
63
86
|
|
|
64
87
|
namespace(:migrate) do
|
|
@@ -66,35 +89,39 @@ apartment_namespace = namespace(:apartment) do
|
|
|
66
89
|
task(up: :environment) do
|
|
67
90
|
Apartment::TaskHelper.warn_if_tenants_empty
|
|
68
91
|
|
|
69
|
-
version = ENV
|
|
92
|
+
version = ENV.fetch('VERSION', nil)&.to_i
|
|
70
93
|
raise('VERSION is required') unless version
|
|
71
94
|
|
|
72
|
-
Apartment::TaskHelper.each_tenant do |tenant|
|
|
95
|
+
results = Apartment::TaskHelper.each_tenant do |tenant|
|
|
73
96
|
puts("Migrating #{tenant} tenant up")
|
|
74
97
|
Apartment::Migrator.run(:up, tenant, version)
|
|
75
|
-
rescue Apartment::TenantNotFound => e
|
|
76
|
-
puts e.message
|
|
77
98
|
end
|
|
99
|
+
|
|
100
|
+
Apartment::TaskHelper.display_summary('Migrate Up', results)
|
|
101
|
+
Apartment::Tasks::SchemaDumper.dump_if_enabled if results.all?(&:success)
|
|
102
|
+
exit(1) if results.any? { |r| !r.success }
|
|
78
103
|
end
|
|
79
104
|
|
|
80
105
|
desc('Runs the "down" for a given migration VERSION across all tenants.')
|
|
81
106
|
task(down: :environment) do
|
|
82
107
|
Apartment::TaskHelper.warn_if_tenants_empty
|
|
83
108
|
|
|
84
|
-
version = ENV
|
|
109
|
+
version = ENV.fetch('VERSION', nil)&.to_i
|
|
85
110
|
raise('VERSION is required') unless version
|
|
86
111
|
|
|
87
|
-
Apartment::TaskHelper.each_tenant do |tenant|
|
|
112
|
+
results = Apartment::TaskHelper.each_tenant do |tenant|
|
|
88
113
|
puts("Migrating #{tenant} tenant down")
|
|
89
114
|
Apartment::Migrator.run(:down, tenant, version)
|
|
90
|
-
rescue Apartment::TenantNotFound => e
|
|
91
|
-
puts e.message
|
|
92
115
|
end
|
|
116
|
+
|
|
117
|
+
Apartment::TaskHelper.display_summary('Migrate Down', results)
|
|
118
|
+
Apartment::Tasks::SchemaDumper.dump_if_enabled if results.all?(&:success)
|
|
119
|
+
exit(1) if results.any? { |r| !r.success }
|
|
93
120
|
end
|
|
94
121
|
|
|
95
122
|
desc('Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).')
|
|
96
123
|
task(:redo) do
|
|
97
|
-
if ENV
|
|
124
|
+
if ENV.fetch('VERSION', nil)
|
|
98
125
|
apartment_namespace['migrate:down'].invoke
|
|
99
126
|
apartment_namespace['migrate:up'].invoke
|
|
100
127
|
else
|
data/ros-apartment.gemspec
CHANGED
|
@@ -32,8 +32,8 @@ Gem::Specification.new do |s|
|
|
|
32
32
|
|
|
33
33
|
s.required_ruby_version = '>= 3.1'
|
|
34
34
|
|
|
35
|
-
s.add_dependency('activerecord', '>=
|
|
36
|
-
s.add_dependency('activesupport', '>=
|
|
35
|
+
s.add_dependency('activerecord', '>= 7.0.0', '< 8.2')
|
|
36
|
+
s.add_dependency('activesupport', '>= 7.0.0', '< 8.2')
|
|
37
37
|
s.add_dependency('parallel', '< 2.0')
|
|
38
38
|
s.add_dependency('public_suffix', '>= 2.0.5', '< 7')
|
|
39
39
|
s.add_dependency('rack', '>= 1.3.6', '< 4.0')
|
metadata
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ros-apartment
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Brunner
|
|
8
8
|
- Brad Robertson
|
|
9
9
|
- Rui Baltazar
|
|
10
10
|
- Mauricio Novelo
|
|
11
|
-
autorequire:
|
|
12
11
|
bindir: bin
|
|
13
12
|
cert_chain: []
|
|
14
|
-
date:
|
|
13
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
14
|
dependencies:
|
|
16
15
|
- !ruby/object:Gem::Dependency
|
|
17
16
|
name: activerecord
|
|
@@ -19,7 +18,7 @@ dependencies:
|
|
|
19
18
|
requirements:
|
|
20
19
|
- - ">="
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version:
|
|
21
|
+
version: 7.0.0
|
|
23
22
|
- - "<"
|
|
24
23
|
- !ruby/object:Gem::Version
|
|
25
24
|
version: '8.2'
|
|
@@ -29,7 +28,7 @@ dependencies:
|
|
|
29
28
|
requirements:
|
|
30
29
|
- - ">="
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
31
|
+
version: 7.0.0
|
|
33
32
|
- - "<"
|
|
34
33
|
- !ruby/object:Gem::Version
|
|
35
34
|
version: '8.2'
|
|
@@ -39,7 +38,7 @@ dependencies:
|
|
|
39
38
|
requirements:
|
|
40
39
|
- - ">="
|
|
41
40
|
- !ruby/object:Gem::Version
|
|
42
|
-
version:
|
|
41
|
+
version: 7.0.0
|
|
43
42
|
- - "<"
|
|
44
43
|
- !ruby/object:Gem::Version
|
|
45
44
|
version: '8.2'
|
|
@@ -49,7 +48,7 @@ dependencies:
|
|
|
49
48
|
requirements:
|
|
50
49
|
- - ">="
|
|
51
50
|
- !ruby/object:Gem::Version
|
|
52
|
-
version:
|
|
51
|
+
version: 7.0.0
|
|
53
52
|
- - "<"
|
|
54
53
|
- !ruby/object:Gem::Version
|
|
55
54
|
version: '8.2'
|
|
@@ -123,6 +122,7 @@ files:
|
|
|
123
122
|
- ".rspec"
|
|
124
123
|
- ".rubocop.yml"
|
|
125
124
|
- ".ruby-version"
|
|
125
|
+
- AGENTS.md
|
|
126
126
|
- Appraisals
|
|
127
127
|
- CLAUDE.md
|
|
128
128
|
- CODE_OF_CONDUCT.md
|
|
@@ -166,7 +166,9 @@ files:
|
|
|
166
166
|
- lib/apartment/migrator.rb
|
|
167
167
|
- lib/apartment/model.rb
|
|
168
168
|
- lib/apartment/railtie.rb
|
|
169
|
+
- lib/apartment/tasks/CLAUDE.md
|
|
169
170
|
- lib/apartment/tasks/enhancements.rb
|
|
171
|
+
- lib/apartment/tasks/schema_dumper.rb
|
|
170
172
|
- lib/apartment/tasks/task_helper.rb
|
|
171
173
|
- lib/apartment/tenant.rb
|
|
172
174
|
- lib/apartment/version.rb
|
|
@@ -181,7 +183,6 @@ licenses:
|
|
|
181
183
|
metadata:
|
|
182
184
|
github_repo: ssh://github.com/rails-on-services/apartment
|
|
183
185
|
rubygems_mfa_required: 'true'
|
|
184
|
-
post_install_message:
|
|
185
186
|
rdoc_options: []
|
|
186
187
|
require_paths:
|
|
187
188
|
- lib
|
|
@@ -196,8 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
196
197
|
- !ruby/object:Gem::Version
|
|
197
198
|
version: '0'
|
|
198
199
|
requirements: []
|
|
199
|
-
rubygems_version: 3.
|
|
200
|
-
signing_key:
|
|
200
|
+
rubygems_version: 3.6.9
|
|
201
201
|
specification_version: 4
|
|
202
202
|
summary: A Ruby gem for managing database multitenancy. Apartment Gem drop in replacement
|
|
203
203
|
test_files: []
|