after_migrate 0.2.3 → 0.2.5

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: b9d807edc34ee3c61bcda0ef4fd2319f80bac36c3bcd1cd1e2a46eb9f5e44f28
4
- data.tar.gz: 16547159bf93c5a192a38a1e92d441bcd9690412b8d58ae163f59f8bc2da71bb
3
+ metadata.gz: 23144c2fac25e5be5c8965155dc54b03b8859063fe00a43d033e9c3bed415f8a
4
+ data.tar.gz: e40e59a694376551d73261e59d214e8ebfe460ee0ab5961116873a22bc9b019f
5
5
  SHA512:
6
- metadata.gz: a7e76304080055fe4b9669d9895b9fb3709266c89e29deb2e39b3d408a29ba88ef1b99ebe951e693da446d2c97fda40ba416b71fae517234b65975e250b6635d
7
- data.tar.gz: 14823ec925953300f85d28586a232111f945c2b9b8fb20bdaa4bf9d3057f8467dec2eb76defda50317a5811bbd6f7071bf71bb195f188950ab2a340030e0b2a5
6
+ metadata.gz: a1ff650538cacd552d26399df13507a0e4f5a71581e32bff0d1e2686ea3d8b8a86e44ef96a7143892ef7cef79a720135489ac174b09ade876b722465b916fc18
7
+ data.tar.gz: 977f7a020023cad34941b24046982d0563f30340cfefe6a196feb83915c7ec5d6a148707cc596749ad0d2ced9a5e64ec55c926d6869d3bd0c853edbd4617dbd8
data/.gitlocalexclude ADDED
@@ -0,0 +1,2 @@
1
+ .ai/
2
+ .claude/
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.4] - 2026-06-01
6
+
7
+ ### Added
8
+ - `config.store_options` for store-specific settings, e.g. `config.store_options_for(:redis)[:client]`
9
+
10
+ ### Changed
11
+ - Store-specific configuration now flows through `config.store_options` while preserving compatibility accessors such as `config.store_path`, `config.redis`, `config.redis_key_prefix`, and `config.redis_ttl`
12
+ - `config.run_id` now defaults from `AFTER_MIGRATE_RUN_ID`, falling back to `default`
13
+
5
14
  ## [0.2.3] - 2026-06-01
6
15
 
7
16
  ### Added
data/CLAUDE.md CHANGED
@@ -22,7 +22,7 @@ This is a Rails gem that automatically runs database maintenance (`ANALYZE`, `VA
22
22
 
23
23
  2. **`Collector`** (`lib/after_migrate/collector.rb`) — receives every SQL notification, filters to DDL/DML statements (CREATE/ALTER/DROP/INSERT/UPDATE/DELETE), calls the adapter-specific parser, and accumulates table names into `AfterMigrate.affected_tables` (a `Concurrent::Map<schema, Concurrent::Set<table_name>>`).
24
24
 
25
- 3. **`AfterMigrate.store`** (`lib/after_migrate/store.rb`) — defaults to an in-memory `Concurrent::Map` wrapped by `AfterMigrate::Stores::Memory`. `AfterMigrate.affected_tables`, `merge_tables`, and `reset!` delegate to the store. The memory store persists across multiple rake task invocations inside one Ruby process and is cleared by `AfterMigrate.run!` (or `AfterMigrate.reset!`). This replaces the old `Current` (`ActiveSupport::CurrentAttributes`) which was reset after every migration.
25
+ 3. **`AfterMigrate.store`** (`lib/after_migrate/store.rb`) — defaults to an in-memory `Concurrent::Map` wrapped by `AfterMigrate::Stores::Memory`. `AfterMigrate.affected_tables`, `merge_tables`, and `reset!` delegate to the store. The memory store persists across multiple rake task invocations inside one Ruby process and is cleared by `AfterMigrate.run!` (or `AfterMigrate.reset!`). This replaces the old `Current` (`ActiveSupport::CurrentAttributes`) which was reset after every migration. Alternate backends: `FileStore` (JSON, cross-invocation) and `RedisStore` (cross-process, for parallel/multi-tenant runs). `RedisStore#resolved_redis` memoizes the resolved client per store instance, so `config.redis` is only materialized once — pass a `ConnectionPool` (the store calls `pool.with`) or a memoized client, never a `-> { Redis.new }` proc that builds a connection per call (that opens one connection per SQL statement and exhausts the Redis connection limit under concurrency).
26
26
 
27
27
  4. **`Executor`** (`lib/after_migrate/executor.rb`) — iterates `AfterMigrate.affected_tables` and calls the correct adapter's `optimize_tables`. Respects the `analyze` config option (`only_affected_tables` / `all_tables` / `none`). Always calls `AfterMigrate.reset!` in its `ensure` block.
28
28
 
@@ -45,13 +45,13 @@ This is a Rails gem that automatically runs database maintenance (`ANALYZE`, `VA
45
45
 
46
46
  Config values are in `AfterMigrate::Configuration` (initialized in `lib/after_migrate.rb`):
47
47
 
48
- | Option | Default | Values |
49
- |-----------------------|--------------------------|----------------------------------------------------|
50
- | `enabled` | `true` | bool |
51
- | `verbose` | `true` | bool |
52
- | `vacuum` | `true` | bool (PostgreSQL only) |
53
- | `analyze` | `"only_affected_tables"` | `"only_affected_tables"`, `"all_tables"`, `"none"` |
54
- | `rake_tasks_enhanced` | `true` | bool |
48
+ | Option | Default | Values |
49
+ |-----------------------|--------------------------|---------------------------------------------------------|
50
+ | `enabled` | `true` | bool |
51
+ | `verbose` | `true` | bool |
52
+ | `vacuum` | `true` | bool (PostgreSQL only) |
53
+ | `analyze` | `"only_affected_tables"` | `"only_affected_tables"`, `"all_tables"`, `"none"` |
54
+ | `rake_tasks_enhanced` | `true` | bool |
55
55
  | `defer` | `true` | bool — skip auto-run; call `AfterMigrate.run!` manually |
56
56
 
57
57
  ## Multi-tenant usage
data/README.md CHANGED
@@ -81,6 +81,41 @@ AfterMigrate.configure do |config|
81
81
  config.rake_tasks_enhanced = true
82
82
  end
83
83
  ```
84
+
85
+ ### Store backends
86
+
87
+ The collected table names are kept in a store. Choose one with `config.store`:
88
+
89
+ - `:memory` (default) — a process-local `Concurrent::Map`. Fine for a single-process migration.
90
+ - `:file` — JSON file, shared across rake invocations in the same run.
91
+ - `:redis` — shared across processes; use this for parallel / multi-tenant migrations.
92
+
93
+ ```ruby
94
+ config.store = :redis
95
+ ```
96
+
97
+ > [!IMPORTANT]
98
+ > **Redis: always pass a pooled or memoized client — never a proc that builds a new connection per call.**
99
+ >
100
+ > `config.redis` is read on *every* collected SQL statement. If it returns a
101
+ > fresh `Redis.new` each time (e.g. `config.redis = -> { Redis.new(...) }`),
102
+ > every statement opens a new, unpooled connection. Under parallel or
103
+ > multi-tenant migrations this exhausts the Redis server connection limit
104
+ > (`Connection refused` / `max number of clients reached`).
105
+ >
106
+ > Recommended — a thread-safe `ConnectionPool` (the store uses `pool.with`):
107
+ >
108
+ > ```ruby
109
+ > config.store = :redis
110
+ > config.redis = ConnectionPool.new(size: 2, timeout: 5) { Redis.new(url: ENV["REDIS_URL"]) }
111
+ > ```
112
+ >
113
+ > A memoized single client also works for sequential migrations:
114
+ >
115
+ > ```ruby
116
+ > config.redis = -> { @after_migrate_redis ||= Redis.new(url: ENV["REDIS_URL"]) }
117
+ > ```
118
+
84
119
  ---
85
120
 
86
121
  ## 🚢 Releasing
@@ -181,12 +181,18 @@ module AfterMigrate
181
181
  block.call(redis)
182
182
  end
183
183
 
184
+ # Memoized so a plain client or a `-> { Redis.new }` proc only ever
185
+ # produces one connection per store instance. Without this, `with_redis`
186
+ # re-invokes the proc on every merge_tables/affected_tables call, opening
187
+ # a fresh unpooled connection per SQL statement — which exhausts the
188
+ # Redis server's connection limit under parallel/multi-tenant migrations.
189
+ # Pass a ConnectionPool as config.redis for thread-safe concurrency.
184
190
  def resolved_redis
185
- client = @redis.respond_to?(:call) ? @redis.call : @redis
186
- return client if client
187
- return Redis.new if defined?(Redis)
188
-
189
- raise 'AfterMigrate Redis store requires config.redis or the redis gem'
191
+ @resolved_redis ||= begin
192
+ client = @redis.respond_to?(:call) ? @redis.call : @redis
193
+ client ||= Redis.new if defined?(Redis)
194
+ client || raise('AfterMigrate Redis store requires config.redis or the redis gem')
195
+ end
190
196
  end
191
197
  end
192
198
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AfterMigrate
4
- VERSION = '0.2.3'
4
+ VERSION = '0.2.5'
5
5
  end
data/lib/after_migrate.rb CHANGED
@@ -9,7 +9,7 @@ require 'after_migrate/railtie'
9
9
  module AfterMigrate
10
10
  class Configuration
11
11
  attr_accessor :enabled, :verbose, :vacuum, :analyze, :rake_tasks_enhanced, :defer,
12
- :store, :store_path, :run_id, :redis, :redis_key_prefix, :redis_ttl
12
+ :store, :run_id, :store_options
13
13
 
14
14
  def initialize
15
15
  @enabled = true
@@ -19,11 +19,53 @@ module AfterMigrate
19
19
  @rake_tasks_enhanced = true
20
20
  @defer = true
21
21
  @store = :memory
22
- @store_path = 'tmp/after_migrate/affected_tables.json'
23
- @run_id = nil
24
- @redis = nil
25
- @redis_key_prefix = 'after_migrate'
26
- @redis_ttl = 24 * 60 * 60
22
+ @run_id = ENV.fetch('AFTER_MIGRATE_RUN_ID', 'default')
23
+ @store_options = {
24
+ file: {
25
+ path: 'tmp/after_migrate/affected_tables.json'
26
+ },
27
+ redis: {
28
+ client: nil,
29
+ key_prefix: 'after_migrate',
30
+ ttl: 24 * 60 * 60
31
+ }
32
+ }
33
+ end
34
+
35
+ def store_path
36
+ store_options_for(:file)[:path]
37
+ end
38
+
39
+ def store_path=(value)
40
+ store_options_for(:file)[:path] = value
41
+ end
42
+
43
+ def redis
44
+ store_options_for(:redis)[:client]
45
+ end
46
+
47
+ def redis=(value)
48
+ store_options_for(:redis)[:client] = value
49
+ end
50
+
51
+ def redis_key_prefix
52
+ store_options_for(:redis)[:key_prefix]
53
+ end
54
+
55
+ def redis_key_prefix=(value)
56
+ store_options_for(:redis)[:key_prefix] = value
57
+ end
58
+
59
+ def redis_ttl
60
+ store_options_for(:redis)[:ttl]
61
+ end
62
+
63
+ def redis_ttl=(value)
64
+ store_options_for(:redis)[:ttl] = value
65
+ end
66
+
67
+ def store_options_for(store_name)
68
+ store_options[store_name.to_sym] ||= {}
27
69
  end
28
70
  end
29
71
 
@@ -72,17 +114,15 @@ module AfterMigrate
72
114
  def store_key
73
115
  [
74
116
  configuration.store.to_s,
75
- configuration.store_path.to_s,
76
117
  configuration.run_id.to_s,
77
- configuration.redis_key_prefix.to_s,
78
- configuration.redis_ttl.to_s
118
+ configuration.store_options_for(configuration.store).hash
79
119
  ]
80
120
  end
81
121
 
82
122
  def build_store
83
123
  case configuration.store.to_s
84
124
  when 'file'
85
- Stores::FileStore.new(path: configuration.store_path, run_id: configuration.run_id)
125
+ file_store
86
126
  when 'redis'
87
127
  redis_store
88
128
  else
@@ -90,12 +130,21 @@ module AfterMigrate
90
130
  end
91
131
  end
92
132
 
133
+ def file_store
134
+ options = configuration.store_options_for(:file)
135
+ Stores::FileStore.new(
136
+ path: options.fetch(:path),
137
+ run_id: options.fetch(:run_id, configuration.run_id)
138
+ )
139
+ end
140
+
93
141
  def redis_store
142
+ options = configuration.store_options_for(:redis)
94
143
  Stores::RedisStore.new(
95
- redis: configuration.redis,
96
- key_prefix: configuration.redis_key_prefix,
97
- run_id: configuration.run_id,
98
- ttl: configuration.redis_ttl
144
+ redis: options[:client],
145
+ key_prefix: options.fetch(:key_prefix),
146
+ run_id: options.fetch(:run_id, configuration.run_id),
147
+ ttl: options.fetch(:ttl)
99
148
  )
100
149
  end
101
150
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: after_migrate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikolay Moskvin
@@ -48,6 +48,7 @@ extensions: []
48
48
  extra_rdoc_files: []
49
49
  files:
50
50
  - ".gitignore"
51
+ - ".gitlocalexclude"
51
52
  - ".rspec"
52
53
  - ".rubocop.yml"
53
54
  - ".ruby-version"