after_migrate 0.2.1 → 0.2.3
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/CHANGELOG.md +32 -0
- data/CLAUDE.md +1 -1
- data/Gemfile +1 -0
- data/lib/after_migrate/executor.rb +18 -9
- data/lib/after_migrate/store.rb +193 -0
- data/lib/after_migrate/version.rb +1 -1
- data/lib/after_migrate.rb +50 -7
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9d807edc34ee3c61bcda0ef4fd2319f80bac36c3bcd1cd1e2a46eb9f5e44f28
|
|
4
|
+
data.tar.gz: 16547159bf93c5a192a38a1e92d441bcd9690412b8d58ae163f59f8bc2da71bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7e76304080055fe4b9669d9895b9fb3709266c89e29deb2e39b3d408a29ba88ef1b99ebe951e693da446d2c97fda40ba416b71fae517234b65975e250b6635d
|
|
7
|
+
data.tar.gz: 14823ec925953300f85d28586a232111f945c2b9b8fb20bdaa4bf9d3057f8467dec2eb76defda50317a5811bbd6f7071bf71bb195f188950ab2a340030e0b2a5
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.3] - 2026-06-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Redis-backed store via `config.store = :redis` for sharing collected migration tables across processes
|
|
9
|
+
- `config.redis` for passing a Redis client, connection pool, or callable client provider
|
|
10
|
+
- `config.redis_key_prefix` and `config.redis_ttl` for namespacing and expiring Redis store keys
|
|
11
|
+
- Redis store support for run isolation via `config.run_id`
|
|
12
|
+
- Redis store specs for persistence, merging, reset behavior, connection pools, `Redis.new` fallback, and missing-client errors
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Store cache keys now include Redis-specific configuration, so changing Redis store settings rebuilds the active store instance
|
|
16
|
+
|
|
17
|
+
## [0.2.2] - 2026-05-31
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Configurable store backend via `config.store`, with `:memory` as the default and `:file` for persistence across separate migration task processes
|
|
21
|
+
- `config.store_path` to control where the file-backed store writes collected table names
|
|
22
|
+
- `config.run_id` to isolate persisted file-store data between independent migration runs
|
|
23
|
+
- File-store locking and atomic writes to avoid corrupting persisted table data during concurrent access
|
|
24
|
+
- Specs for file-store persistence, corrupt JSON handling, adapter dispatch, unsupported adapters, and deferred rake task behavior
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `AfterMigrate.affected_tables`, `AfterMigrate.merge_tables`, and `AfterMigrate.reset!` now delegate through the configured store backend
|
|
28
|
+
- Executor resets the store when maintenance is disabled or no tables are pending, but keeps collected tables when adapter optimization raises so a later run can retry
|
|
29
|
+
|
|
30
|
+
## [0.2.1] - 2026-05-29
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Refactored SQL identifier matching used by the parser without changing supported table-detection behavior
|
|
34
|
+
- Split executor migration logging message construction into a local variable for clearer formatting and maintenance
|
|
35
|
+
- Added RuboCop project configuration for documentation, method length, and spec block length rules
|
|
36
|
+
|
|
5
37
|
## [0.2.0] - 2026-04-28
|
|
6
38
|
|
|
7
39
|
### 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.
|
|
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.
|
|
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
|
|
data/Gemfile
CHANGED
|
@@ -12,24 +12,33 @@ module AfterMigrate
|
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
14
|
def call(schema: nil)
|
|
15
|
-
return unless
|
|
15
|
+
return reset_store unless maintenance_enabled?
|
|
16
16
|
|
|
17
17
|
tables = target_tables
|
|
18
|
-
return if tables.blank?
|
|
18
|
+
return reset_store if tables.blank?
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else
|
|
23
|
-
tables.each { |s, t| run_optimize(schema: s, tables: t) }
|
|
24
|
-
end
|
|
25
|
-
ensure
|
|
26
|
-
AfterMigrate.reset!
|
|
20
|
+
run_optimizations(schema:, tables:)
|
|
21
|
+
reset_store
|
|
27
22
|
end
|
|
28
23
|
|
|
29
24
|
public :call
|
|
30
25
|
|
|
31
26
|
private
|
|
32
27
|
|
|
28
|
+
def maintenance_enabled?
|
|
29
|
+
AfterMigrate.configuration.vacuum || AfterMigrate.configuration.analyze != 'none'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_store
|
|
33
|
+
AfterMigrate.reset!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run_optimizations(schema:, tables:)
|
|
37
|
+
return run_optimize(schema:, tables: tables[schema]) if schema.present? && tables[schema].present?
|
|
38
|
+
|
|
39
|
+
tables.each { |s, t| run_optimize(schema: s, tables: t) } unless schema.present?
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
def run_optimize(schema:, tables:)
|
|
34
43
|
table_names = tables.to_a.sort
|
|
35
44
|
return if table_names.empty?
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module AfterMigrate
|
|
7
|
+
module Stores
|
|
8
|
+
class Memory
|
|
9
|
+
attr_reader :affected_tables
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@affected_tables = Concurrent::Map.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def merge_tables(schema, table_names)
|
|
16
|
+
return if table_names.blank?
|
|
17
|
+
|
|
18
|
+
set = affected_tables.compute_if_absent(schema) { Concurrent::Set.new }
|
|
19
|
+
set.merge(table_names)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset!
|
|
23
|
+
@affected_tables = Concurrent::Map.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class FileStore < Memory
|
|
28
|
+
attr_reader :path, :run_id
|
|
29
|
+
|
|
30
|
+
def initialize(path:, run_id: nil)
|
|
31
|
+
super()
|
|
32
|
+
@path = path.to_s
|
|
33
|
+
@run_id = run_id.to_s.presence
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def affected_tables
|
|
37
|
+
load_into_memory
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def merge_tables(schema, table_names)
|
|
42
|
+
return if table_names.blank?
|
|
43
|
+
|
|
44
|
+
with_lock do
|
|
45
|
+
merge_into_memory(read_schemas)
|
|
46
|
+
merge_into_memory(schema => table_names)
|
|
47
|
+
write_schemas(memory_to_hash)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reset!
|
|
52
|
+
super
|
|
53
|
+
with_lock { ::FileUtils.rm_f(path) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def load_into_memory
|
|
59
|
+
with_lock { merge_into_memory(read_schemas) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def merge_into_memory(schemas)
|
|
63
|
+
schemas.each do |schema, table_names|
|
|
64
|
+
set = @affected_tables.compute_if_absent(schema) { Concurrent::Set.new }
|
|
65
|
+
set.merge(table_names)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_schemas
|
|
70
|
+
return {} unless ::File.exist?(path)
|
|
71
|
+
|
|
72
|
+
payload = JSON.parse(::File.read(path))
|
|
73
|
+
return {} if run_id && payload['run_id'].to_s != run_id
|
|
74
|
+
|
|
75
|
+
payload.fetch('schemas', {})
|
|
76
|
+
rescue JSON::ParserError
|
|
77
|
+
{}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def write_schemas(schemas)
|
|
81
|
+
::FileUtils.mkdir_p(::File.dirname(path))
|
|
82
|
+
temp_path = "#{path}.#{$PROCESS_ID}.tmp"
|
|
83
|
+
::File.write(temp_path, JSON.pretty_generate({ run_id:, schemas: }))
|
|
84
|
+
::File.rename(temp_path, path)
|
|
85
|
+
ensure
|
|
86
|
+
::FileUtils.rm_f(temp_path) if temp_path && ::File.exist?(temp_path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def memory_to_hash
|
|
90
|
+
@affected_tables.keys.sort.each_with_object({}) do |schema, hash|
|
|
91
|
+
hash[schema] = @affected_tables[schema].to_a.sort
|
|
92
|
+
end.sort.to_h
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def with_lock
|
|
96
|
+
::FileUtils.mkdir_p(::File.dirname(path))
|
|
97
|
+
::File.open("#{path}.lock", ::File::RDWR | ::File::CREAT, 0o644) do |file|
|
|
98
|
+
file.flock(::File::LOCK_EX)
|
|
99
|
+
yield
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class RedisStore < Memory
|
|
105
|
+
attr_reader :key_prefix, :run_id, :ttl
|
|
106
|
+
|
|
107
|
+
def initialize(redis:, key_prefix: 'after_migrate', run_id: nil, ttl: 24 * 60 * 60)
|
|
108
|
+
super()
|
|
109
|
+
@redis = redis
|
|
110
|
+
@key_prefix = key_prefix.to_s
|
|
111
|
+
@run_id = run_id.to_s.presence || 'default'
|
|
112
|
+
@ttl = ttl.to_i
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def affected_tables
|
|
116
|
+
load_into_memory
|
|
117
|
+
super
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def merge_tables(schema, table_names)
|
|
121
|
+
return if table_names.blank?
|
|
122
|
+
|
|
123
|
+
with_redis do |redis|
|
|
124
|
+
redis.sadd(index_key, schema)
|
|
125
|
+
redis.sadd(schema_key(schema), table_names.to_a)
|
|
126
|
+
expire_keys(redis, schema)
|
|
127
|
+
end
|
|
128
|
+
merge_into_memory(schema => table_names)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def reset!
|
|
132
|
+
super
|
|
133
|
+
with_redis do |redis|
|
|
134
|
+
schemas = redis.smembers(index_key)
|
|
135
|
+
keys = schemas.map { |schema| schema_key(schema) }
|
|
136
|
+
redis.del(*(keys + [index_key])) if keys.any?
|
|
137
|
+
redis.del(index_key) if keys.empty?
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def load_into_memory
|
|
144
|
+
with_redis do |redis|
|
|
145
|
+
redis.smembers(index_key).each do |schema|
|
|
146
|
+
merge_into_memory(schema => redis.smembers(schema_key(schema)))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def merge_into_memory(schemas)
|
|
152
|
+
schemas.each do |schema, table_names|
|
|
153
|
+
set = @affected_tables.compute_if_absent(schema) { Concurrent::Set.new }
|
|
154
|
+
set.merge(table_names)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def index_key
|
|
159
|
+
"#{base_key}:schemas"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def schema_key(schema)
|
|
163
|
+
"#{base_key}:schema:#{schema}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def base_key
|
|
167
|
+
"#{key_prefix}:#{run_id}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def expire_keys(redis, schema)
|
|
171
|
+
return unless ttl.positive?
|
|
172
|
+
|
|
173
|
+
redis.expire(index_key, ttl)
|
|
174
|
+
redis.expire(schema_key(schema), ttl)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def with_redis(&block)
|
|
178
|
+
redis = resolved_redis
|
|
179
|
+
return redis.with { |connection| block.call(connection) } if redis.respond_to?(:with)
|
|
180
|
+
|
|
181
|
+
block.call(redis)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
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'
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
data/lib/after_migrate.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'after_migrate/version'
|
|
4
|
+
require 'after_migrate/store'
|
|
4
5
|
require 'after_migrate/collector'
|
|
5
6
|
require 'after_migrate/executor'
|
|
6
7
|
require 'after_migrate/railtie'
|
|
7
8
|
|
|
8
9
|
module AfterMigrate
|
|
9
10
|
class Configuration
|
|
10
|
-
attr_accessor :enabled, :verbose, :vacuum, :analyze, :rake_tasks_enhanced, :defer
|
|
11
|
+
attr_accessor :enabled, :verbose, :vacuum, :analyze, :rake_tasks_enhanced, :defer,
|
|
12
|
+
:store, :store_path, :run_id, :redis, :redis_key_prefix, :redis_ttl
|
|
11
13
|
|
|
12
14
|
def initialize
|
|
13
15
|
@enabled = true
|
|
@@ -16,6 +18,12 @@ module AfterMigrate
|
|
|
16
18
|
@analyze = 'only_affected_tables'
|
|
17
19
|
@rake_tasks_enhanced = true
|
|
18
20
|
@defer = true
|
|
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
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
|
|
@@ -34,14 +42,11 @@ module AfterMigrate
|
|
|
34
42
|
|
|
35
43
|
# Persistent cross-migration store: schema_name => Concurrent::Set<table_name>
|
|
36
44
|
def affected_tables
|
|
37
|
-
|
|
45
|
+
store.affected_tables
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def merge_tables(schema, table_names)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
set = affected_tables.compute_if_absent(schema) { Concurrent::Set.new }
|
|
44
|
-
set.merge(table_names)
|
|
49
|
+
store.merge_tables(schema, table_names)
|
|
45
50
|
end
|
|
46
51
|
|
|
47
52
|
# Trigger database maintenance on all collected tables, then clear the store.
|
|
@@ -53,7 +58,45 @@ module AfterMigrate
|
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
def reset!
|
|
56
|
-
|
|
61
|
+
store.reset!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def store
|
|
65
|
+
@store = nil if @store_key != store_key
|
|
66
|
+
@store_key = store_key
|
|
67
|
+
@store ||= build_store
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def store_key
|
|
73
|
+
[
|
|
74
|
+
configuration.store.to_s,
|
|
75
|
+
configuration.store_path.to_s,
|
|
76
|
+
configuration.run_id.to_s,
|
|
77
|
+
configuration.redis_key_prefix.to_s,
|
|
78
|
+
configuration.redis_ttl.to_s
|
|
79
|
+
]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_store
|
|
83
|
+
case configuration.store.to_s
|
|
84
|
+
when 'file'
|
|
85
|
+
Stores::FileStore.new(path: configuration.store_path, run_id: configuration.run_id)
|
|
86
|
+
when 'redis'
|
|
87
|
+
redis_store
|
|
88
|
+
else
|
|
89
|
+
Stores::Memory.new
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def redis_store
|
|
94
|
+
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
|
|
99
|
+
)
|
|
57
100
|
end
|
|
58
101
|
end
|
|
59
102
|
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.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nikolay Moskvin
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- lib/after_migrate/collector.rb
|
|
70
70
|
- lib/after_migrate/executor.rb
|
|
71
71
|
- lib/after_migrate/railtie.rb
|
|
72
|
+
- lib/after_migrate/store.rb
|
|
72
73
|
- lib/after_migrate/version.rb
|
|
73
74
|
- logo.png
|
|
74
75
|
homepage: https://github.com/moskvin/after_migrate
|