ros-apartment 3.4.0 → 3.4.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af86a04df8bf00d04859470cd76a8228d6b984da90e1324ed9eff43cbd0db0ac
4
- data.tar.gz: 905caaf498b70015ddccc9d8846ec18384fac0a98f6dd1010890a8fa2fb19e97
3
+ metadata.gz: 2ff5e16756f5fc42560082bd07204b8853c85a4b4d15ce38664585b2e6631ccc
4
+ data.tar.gz: 168b28a0141dca9bdbdc80ea6ecf3247677b7dc138ca6d3feed213d24c11b454
5
5
  SHA512:
6
- metadata.gz: dfea9df160a8eb0e400f215eada4ecfb41852ab4d92123e1ee51bf2f1a25069c24a36a6b471048d70091a03af5efd420af6657e6bba38748630f285a5aa344cf
7
- data.tar.gz: 9a521c51af92800ac7b42a2f7f7ef48322d075095aed7868807caecc248958fc0667e3c0ae22b14e17d9df44ada1c325ca9804e03bdc9947a2f90bcf1a6bb27a
6
+ metadata.gz: c8899404718944dae59cc218ac48c558f6005cce6bb6f61357bca1f3a69e8e9a0bbef7950d60effcc1a02efde0ecc65062fc258565a5375a7991cc3ad2d0a293
7
+ data.tar.gz: 4b4aa74a1194609b254a3e08c00d493243842396f045560d6909ae3c67751dbb183d56545046bb61508f61e039ee2159875fe8fb2766d15f20cea28068a8504a
data/RELEASING.md ADDED
@@ -0,0 +1,106 @@
1
+ # Releasing ros-apartment
2
+
3
+ This document describes the release process for the `ros-apartment` gem.
4
+
5
+ ## Overview
6
+
7
+ Releases are automated via GitHub Actions. Pushing to `main` triggers the `gem-publish.yml` workflow, which publishes to RubyGems using trusted publishing (no API key required).
8
+
9
+ ## Prerequisites
10
+
11
+ - All changes merged to `development` branch
12
+ - CI passing on `development`
13
+ - Version number updated in `lib/apartment/version.rb`
14
+
15
+ ## Release Steps
16
+
17
+ ### 1. Bump the version
18
+
19
+ Update `lib/apartment/version.rb` on the `development` branch:
20
+
21
+ ```ruby
22
+ module Apartment
23
+ VERSION = 'X.Y.Z'
24
+ end
25
+ ```
26
+
27
+ Follow [Semantic Versioning](https://semver.org/):
28
+ - **MAJOR** (X): Breaking changes
29
+ - **MINOR** (Y): New features, backwards compatible
30
+ - **PATCH** (Z): Bug fixes, backwards compatible
31
+
32
+ ### 2. Create release PR
33
+
34
+ Create a PR from `development` to `main`:
35
+
36
+ ```bash
37
+ gh pr create --base main --head development --title "Release vX.Y.Z"
38
+ ```
39
+
40
+ Include a summary of changes in the PR description.
41
+
42
+ ### 3. Merge the release PR
43
+
44
+ Once CI passes and the PR is approved, merge it. This triggers the publish workflow.
45
+
46
+ **Important**: The workflow creates the git tag automatically. Do not create the tag manually beforehand or the workflow will fail.
47
+
48
+ ### 4. Verify the publish
49
+
50
+ Monitor the `gem-publish.yml` workflow run. It will:
51
+ 1. Build the gem
52
+ 2. Create and push the `vX.Y.Z` tag
53
+ 3. Publish to RubyGems
54
+ 4. Wait for RubyGems indexes to update
55
+
56
+ Verify at: https://rubygems.org/gems/ros-apartment
57
+
58
+ ### 5. Create GitHub Release
59
+
60
+ After the workflow completes:
61
+
62
+ 1. Go to https://github.com/rails-on-services/apartment/releases/new
63
+ 2. Select the `vX.Y.Z` tag (created by the workflow)
64
+ 3. Click "Generate release notes" for a starting point
65
+ 4. Edit the release notes to highlight key changes
66
+ 5. Publish the release
67
+
68
+ We use GitHub Releases as our changelog (no CHANGELOG.md file).
69
+
70
+ ### 6. Sync branches
71
+
72
+ Merge `main` back into `development` to keep them in sync:
73
+
74
+ ```bash
75
+ git checkout development
76
+ git pull origin development
77
+ git merge origin/main --no-edit
78
+ git push
79
+ ```
80
+
81
+ ## Workflow Details
82
+
83
+ The `gem-publish.yml` workflow uses:
84
+ - **Trusted publishing**: Configured via RubyGems.org OIDC, no API key needed
85
+ - **rubygems/release-gem@v1**: Official RubyGems action
86
+ - **rake release**: Builds gem, creates tag, pushes to RubyGems
87
+
88
+ ## Troubleshooting
89
+
90
+ ### Workflow fails with "tag already exists"
91
+
92
+ The tag was created manually before the workflow ran. Delete the tag and re-run:
93
+
94
+ ```bash
95
+ git push origin --delete vX.Y.Z
96
+ ```
97
+
98
+ Then re-trigger the workflow by pushing to main again (or re-run from GitHub Actions UI).
99
+
100
+ ### Gem published but GitHub Release missing
101
+
102
+ The GitHub Release is created manually (step 5). The gem is already available on RubyGems; the release is just for documentation.
103
+
104
+ ### RubyGems trusted publishing fails
105
+
106
+ Verify the GitHub environment `production` is configured correctly in repository settings, and that RubyGems.org has the trusted publisher configured for this repository.
data/context7.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "https://context7.com/rails-on-services/apartment",
3
+ "public_key": "pk_EQhqzkh8FktmxBU0mbzmZ"
4
+ }
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Style/ClassAndModuleChildren
3
+ # rubocop:disable Style/ClassAndModuleChildren, Style/OneClassPerFile
4
4
 
5
5
  # NOTE: This patch is meant to remove any schema_prefix appart from the ones for
6
6
  # excluded models. The schema_prefix would be resolved by apartment's setting
@@ -55,4 +55,4 @@ require 'active_record/connection_adapters/postgresql_adapter'
55
55
  class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
56
56
  include Apartment::PostgreSqlAdapterPatch
57
57
  end
58
- # rubocop:enable Style/ClassAndModuleChildren
58
+ # rubocop:enable Style/ClassAndModuleChildren, Style/OneClassPerFile
@@ -41,7 +41,7 @@ module Apartment
41
41
  raise(ActiveRecord::StatementInvalid, "Could not find schema #{tenant}") unless schema_exists?(tenant)
42
42
 
43
43
  @current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
44
- Apartment.connection.schema_search_path = full_search_path
44
+ set_schema_search_path(full_search_path)
45
45
  rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError
46
46
  raise(TenantNotFound, "One of the following schema(s) is invalid: #{full_search_path}")
47
47
  end
@@ -41,12 +41,12 @@ module Apartment
41
41
  #
42
42
  def reset
43
43
  @current = default_tenant
44
- Apartment.connection.schema_search_path = full_search_path
44
+ set_schema_search_path(full_search_path)
45
45
  end
46
46
 
47
47
  def init
48
48
  super
49
- Apartment.connection.schema_search_path = full_search_path
49
+ set_schema_search_path(full_search_path)
50
50
  end
51
51
 
52
52
  def current
@@ -76,13 +76,35 @@ module Apartment
76
76
  raise(ActiveRecord::StatementInvalid, "Could not find schema #{tenant}") unless schema_exists?(tenant)
77
77
 
78
78
  @current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
79
- Apartment.connection.schema_search_path = full_search_path
79
+ set_schema_search_path(full_search_path)
80
80
  rescue *rescuable_exceptions => e
81
81
  raise_schema_connect_to_new(tenant, e)
82
82
  end
83
83
 
84
84
  private
85
85
 
86
+ # Force a fresh +SET search_path+ even when ActiveRecord's
87
+ # +@schema_search_path+ cache still matches +value+. Rails 8.1 added a
88
+ # memoization early-return to PostgreSQLAdapter#schema_search_path= (see
89
+ # rails/rails#54698), which makes the assignment a no-op when the ivar
90
+ # is unchanged. After a transactional ROLLBACK or a connection reconnect
91
+ # the ivar can hold a stale value while the actual session search_path
92
+ # has reverted, so we must invalidate the ivar before reassigning.
93
+ # This is a cache-invalidating wrapper, not a plain writer.
94
+ #
95
+ # Maintenance note: this couples us to Rails' private
96
+ # +@schema_search_path+ ivar. Watch rails/rails#54698 for an upstream
97
+ # fix or a public invalidation API -- once one ships, this workaround
98
+ # should be dropped in favor of the public path. If a future Rails
99
+ # release renames or removes the ivar, this helper degrades silently
100
+ # back to the memoization no-op; the regression specs under
101
+ # +describe '#switch!'+ and +describe '#reset'+ in
102
+ # +spec/examples/schema_adapter_examples.rb+ are the canary.
103
+ def set_schema_search_path(value) # rubocop:disable Naming/AccessorMethodName
104
+ Apartment.connection.instance_variable_set(:@schema_search_path, nil)
105
+ Apartment.connection.schema_search_path = value
106
+ end
107
+
86
108
  def tenant_exists?(tenant)
87
109
  return true unless Apartment.tenant_presence_check
88
110
 
@@ -8,37 +8,47 @@ module Apartment
8
8
 
9
9
  # Migrate to latest
10
10
  def migrate(database)
11
- Tenant.switch(database) do
12
- version = ENV['VERSION']&.to_i
11
+ # Pin a connection for the entire migration to ensure Tenant.switch
12
+ # sets search_path on the same connection used by migration_context.
13
+ # Without this, connection pool may return different connections
14
+ # for the switch vs the actual migration operations.
15
+ ActiveRecord::Base.connection_pool.with_connection do
16
+ Tenant.switch(database) do
17
+ version = ENV['VERSION']&.to_i
13
18
 
14
- migration_scope_block = ->(migration) { ENV['SCOPE'].blank? || (ENV['SCOPE'] == migration.scope) }
19
+ migration_scope_block = ->(migration) { ENV['SCOPE'].blank? || (ENV['SCOPE'] == migration.scope) }
15
20
 
16
- if ActiveRecord.version >= Gem::Version.new('7.2.0')
17
- ActiveRecord::Base.connection_pool.migration_context.migrate(version, &migration_scope_block)
18
- else
19
- ActiveRecord::Base.connection.migration_context.migrate(version, &migration_scope_block)
21
+ if ActiveRecord.version >= Gem::Version.new('7.2.0')
22
+ ActiveRecord::Base.connection_pool.migration_context.migrate(version, &migration_scope_block)
23
+ else
24
+ ActiveRecord::Base.connection.migration_context.migrate(version, &migration_scope_block)
25
+ end
20
26
  end
21
27
  end
22
28
  end
23
29
 
24
30
  # Migrate up/down to a specific version
25
31
  def run(direction, database, version)
26
- Tenant.switch(database) do
27
- if ActiveRecord.version >= Gem::Version.new('7.2.0')
28
- ActiveRecord::Base.connection_pool.migration_context.run(direction, version)
29
- else
30
- ActiveRecord::Base.connection.migration_context.run(direction, version)
32
+ ActiveRecord::Base.connection_pool.with_connection do
33
+ Tenant.switch(database) do
34
+ if ActiveRecord.version >= Gem::Version.new('7.2.0')
35
+ ActiveRecord::Base.connection_pool.migration_context.run(direction, version)
36
+ else
37
+ ActiveRecord::Base.connection.migration_context.run(direction, version)
38
+ end
31
39
  end
32
40
  end
33
41
  end
34
42
 
35
43
  # rollback latest migration `step` number of times
36
44
  def rollback(database, step = 1)
37
- Tenant.switch(database) do
38
- if ActiveRecord.version >= Gem::Version.new('7.2.0')
39
- ActiveRecord::Base.connection_pool.migration_context.rollback(step)
40
- else
41
- ActiveRecord::Base.connection.migration_context.rollback(step)
45
+ ActiveRecord::Base.connection_pool.with_connection do
46
+ Tenant.switch(database) do
47
+ if ActiveRecord.version >= Gem::Version.new('7.2.0')
48
+ ActiveRecord::Base.connection_pool.migration_context.rollback(step)
49
+ else
50
+ ActiveRecord::Base.connection.migration_context.rollback(step)
51
+ end
42
52
  end
43
53
  end
44
54
  end
@@ -2,12 +2,26 @@
2
2
 
3
3
  # Require this file to append Apartment rake tasks to ActiveRecord db rake tasks
4
4
  # Enabled by default in the initializer
5
+ #
6
+ # ## Multi-Database Support (Rails 7+)
7
+ #
8
+ # When a Rails app has multiple databases configured in database.yml, Rails creates
9
+ # namespaced rake tasks like `db:migrate:primary`, `db:rollback:primary`, etc.
10
+ # This enhancer automatically detects databases with `database_tasks: true` and
11
+ # enhances their namespaced tasks to also run the corresponding apartment task.
12
+ #
13
+ # Example: Running `rails db:rollback:primary` will also invoke `apartment:rollback`
14
+ # to rollback all tenant schemas.
5
15
 
6
16
  module Apartment
7
17
  class RakeTaskEnhancer
8
18
  module TASKS
9
19
  ENHANCE_BEFORE = %w[db:drop].freeze
10
20
  ENHANCE_AFTER = %w[db:migrate db:rollback db:migrate:up db:migrate:down db:migrate:redo db:seed].freeze
21
+
22
+ # Base tasks that have namespaced variants in multi-database setups
23
+ # db:seed is excluded because Rails doesn't create db:seed:primary
24
+ NAMESPACED_AFTER = %w[db:migrate db:rollback db:migrate:up db:migrate:down db:migrate:redo].freeze
11
25
  freeze
12
26
  end
13
27
 
@@ -18,36 +32,89 @@ module Apartment
18
32
  def enhance!
19
33
  return unless should_enhance?
20
34
 
21
- # insert task before
35
+ enhance_base_tasks!
36
+ enhance_namespaced_tasks!
37
+ end
38
+
39
+ def should_enhance?
40
+ Apartment.db_migrate_tenants
41
+ end
42
+
43
+ private
44
+
45
+ # Enhance standard db:* tasks (backward compatible behavior)
46
+ def enhance_base_tasks!
22
47
  TASKS::ENHANCE_BEFORE.each do |name|
23
- task = Rake::Task[name]
24
- enhance_before_task(task)
48
+ enhance_task_before(name)
25
49
  end
26
50
 
27
- # insert task after
28
51
  TASKS::ENHANCE_AFTER.each do |name|
29
- task = Rake::Task[name]
30
- enhance_after_task(task)
52
+ enhance_task_after(name)
31
53
  end
32
54
  end
33
55
 
34
- def should_enhance?
35
- Apartment.db_migrate_tenants
56
+ # Enhance namespaced db:*:database_name tasks for multi-database setups
57
+ # Maps namespaced tasks to base apartment tasks:
58
+ # db:migrate:primary -> apartment:migrate
59
+ # db:rollback:primary -> apartment:rollback
60
+ # db:migrate:up:primary -> apartment:migrate:up
61
+ def enhance_namespaced_tasks!
62
+ database_names_with_tasks.each do |db_name|
63
+ TASKS::NAMESPACED_AFTER.each do |base_task|
64
+ namespaced_task = "#{base_task}:#{db_name}"
65
+ next unless task_defined?(namespaced_task)
66
+
67
+ apartment_task = base_task.sub('db:', 'apartment:')
68
+ enhance_namespaced_task_after(namespaced_task, apartment_task)
69
+ end
70
+ end
36
71
  end
37
72
 
38
- def enhance_before_task(task)
73
+ def enhance_task_before(name)
74
+ return unless task_defined?(name)
75
+
76
+ task = Rake::Task[name]
39
77
  task.enhance([inserted_task_name(task)])
40
78
  end
41
79
 
42
- def enhance_after_task(task)
80
+ def enhance_task_after(name)
81
+ return unless task_defined?(name)
82
+
83
+ task = Rake::Task[name]
43
84
  task.enhance do
44
85
  Rake::Task[inserted_task_name(task)].invoke
45
86
  end
46
87
  end
47
88
 
89
+ def enhance_namespaced_task_after(namespaced_task_name, apartment_task_name)
90
+ Rake::Task[namespaced_task_name].enhance do
91
+ Rake::Task[apartment_task_name].invoke
92
+ end
93
+ end
94
+
48
95
  def inserted_task_name(task)
49
96
  task.name.sub('db:', 'apartment:')
50
97
  end
98
+
99
+ def task_defined?(name)
100
+ Rake::Task.task_defined?(name)
101
+ end
102
+
103
+ # Returns database names that have database_tasks enabled and are not replicas.
104
+ # These are the databases for which Rails creates namespaced rake tasks.
105
+ #
106
+ # @return [Array<String>] database names (e.g., ['primary', 'secondary'])
107
+ def database_names_with_tasks
108
+ return [] unless defined?(Rails) && Rails.respond_to?(:env)
109
+
110
+ configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
111
+ configs
112
+ .select { |c| c.database_tasks? && !c.replica? }
113
+ .map(&:name)
114
+ rescue StandardError
115
+ # Fail gracefully if configurations unavailable (e.g., during early boot)
116
+ []
117
+ end
51
118
  end
52
119
  end
53
120
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apartment
4
- VERSION = '3.4.0'
4
+ VERSION = '3.4.2'
5
5
  end
data/lib/apartment.rb CHANGED
@@ -164,17 +164,22 @@ module Apartment
164
164
  end
165
165
 
166
166
  # Exceptions
167
- ApartmentError = Class.new(StandardError)
167
+ class ApartmentError < StandardError
168
+ end
168
169
 
169
170
  # Raised when apartment cannot find the adapter specified in <tt>config/database.yml</tt>
170
- AdapterNotFound = Class.new(ApartmentError)
171
+ class AdapterNotFound < ApartmentError
172
+ end
171
173
 
172
174
  # Raised when apartment cannot find the file to be loaded
173
- FileNotFound = Class.new(ApartmentError)
175
+ class FileNotFound < ApartmentError
176
+ end
174
177
 
175
178
  # Tenant specified is unknown
176
- TenantNotFound = Class.new(ApartmentError)
179
+ class TenantNotFound < ApartmentError
180
+ end
177
181
 
178
182
  # The Tenant attempting to be created already exists
179
- TenantExists = Class.new(ApartmentError)
183
+ class TenantExists < ApartmentError
184
+ end
180
185
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ros-apartment
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Brunner
@@ -129,7 +129,9 @@ files:
129
129
  - Gemfile
130
130
  - Guardfile
131
131
  - README.md
132
+ - RELEASING.md
132
133
  - Rakefile
134
+ - context7.json
133
135
  - docs/adapters.md
134
136
  - docs/architecture.md
135
137
  - docs/elevators.md