after_migrate 0.2.1 → 0.2.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: 19aeb8ce5d84227e4ea467f7e0d14ff8ccdb34f49214f9201b7550ede7ffbbf3
4
- data.tar.gz: 2c52a7d680cb37bc2536be8ccd822fee3b92fc6e2b91a5587fa7da1826d14993
3
+ metadata.gz: 62f5a309584cee52788e0ea45c222648cc58c4db46bc08ce6188c6325f6c62dc
4
+ data.tar.gz: e36efca18e936086cdace33a4fe422c2d34f63502ddf01c53090cf8048ed92c6
5
5
  SHA512:
6
- metadata.gz: f8f671d511d7dc8cecc9a33e8bd90752cd0bb5b5ccb50a485dd7215c50ee68d76ec9ce626850d18717bb507a50e0bfe31e71425d24975607144b3749da1d9345
7
- data.tar.gz: 80e42d103e33f52644366ebd660a0d5252c55934386830c68f33c9418b37ffbb92310c1c2dc2dd033d9a62204f0bd8d8c78f222852796d5838c4526ae313809e
6
+ metadata.gz: 2590c965f71fc07b5171cea88869115516687331138b0a6a20b5c4bdd3008a13be1c7a7382dd8886112a02e076904d57f59e2f24db44bfa23d1c36dd30700b40
7
+ data.tar.gz: 4a2f1dd2df2bb18e8f663b1ca150db22a6d4d5474b651ea0ee693129640926266ce77168e370921cab4ac214620a601fe02f6204a97829575ec7e417c944a7d4
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
  .idea/
13
13
  /Gemfile.lock
14
+ .private/
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.2] - 2026-05-31
6
+
7
+ ### Added
8
+ - Configurable store backend via `config.store`, with `:memory` as the default and `:file` for persistence across separate migration task processes
9
+ - `config.store_path` to control where the file-backed store writes collected table names
10
+ - `config.run_id` to isolate persisted file-store data between independent migration runs
11
+ - File-store locking and atomic writes to avoid corrupting persisted table data during concurrent access
12
+ - Specs for file-store persistence, corrupt JSON handling, adapter dispatch, unsupported adapters, and deferred rake task behavior
13
+
14
+ ### Changed
15
+ - `AfterMigrate.affected_tables`, `AfterMigrate.merge_tables`, and `AfterMigrate.reset!` now delegate through the configured store backend
16
+ - 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
17
+
18
+ ## [0.2.1] - 2026-05-29
19
+
20
+ ### Changed
21
+ - Refactored SQL identifier matching used by the parser without changing supported table-detection behavior
22
+ - Split executor migration logging message construction into a local variable for clearer formatting and maintenance
23
+ - Added RuboCop project configuration for documentation, method length, and spec block length rules
24
+
5
25
  ## [0.2.0] - 2026-04-28
6
26
 
7
27
  ### 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.affected_tables`** (module-level store in `lib/after_migrate.rb`) — a `Concurrent::Map` that persists across multiple rake task invocations. Only cleared when `AfterMigrate.run!` (or `AfterMigrate.reset!`) is called. 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.
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
@@ -11,3 +11,4 @@ gem 'bundler', '~> 4'
11
11
  gem 'rake', '~> 13.0'
12
12
  gem 'rspec', '~> 3.0'
13
13
  gem 'rubocop', '~> 1.81'
14
+ gem 'simplecov', group: 'test'
@@ -12,24 +12,33 @@ module AfterMigrate
12
12
  module_function
13
13
 
14
14
  def call(schema: nil)
15
- return unless AfterMigrate.configuration.vacuum || AfterMigrate.configuration.analyze != 'none'
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
- if schema.present?
21
- run_optimize(schema:, tables: tables[schema]) if tables[schema].present?
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,104 @@
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
+ end
104
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AfterMigrate
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
data/lib/after_migrate.rb CHANGED
@@ -1,13 +1,14 @@
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, :store, :store_path, :run_id
11
12
 
12
13
  def initialize
13
14
  @enabled = true
@@ -16,6 +17,9 @@ module AfterMigrate
16
17
  @analyze = 'only_affected_tables'
17
18
  @rake_tasks_enhanced = true
18
19
  @defer = true
20
+ @store = :memory
21
+ @store_path = 'tmp/after_migrate/affected_tables.json'
22
+ @run_id = nil
19
23
  end
20
24
  end
21
25
 
@@ -34,14 +38,11 @@ module AfterMigrate
34
38
 
35
39
  # Persistent cross-migration store: schema_name => Concurrent::Set<table_name>
36
40
  def affected_tables
37
- @affected_tables ||= Concurrent::Map.new
41
+ store.affected_tables
38
42
  end
39
43
 
40
44
  def merge_tables(schema, table_names)
41
- return if table_names.blank?
42
-
43
- set = affected_tables.compute_if_absent(schema) { Concurrent::Set.new }
44
- set.merge(table_names)
45
+ store.merge_tables(schema, table_names)
45
46
  end
46
47
 
47
48
  # Trigger database maintenance on all collected tables, then clear the store.
@@ -53,7 +54,25 @@ module AfterMigrate
53
54
  end
54
55
 
55
56
  def reset!
56
- @affected_tables = nil
57
+ store.reset!
58
+ end
59
+
60
+ def store
61
+ key = [configuration.store.to_s, configuration.store_path.to_s, configuration.run_id.to_s]
62
+ @store = nil if @store_key != key
63
+ @store_key = key
64
+ @store ||= build_store
65
+ end
66
+
67
+ private
68
+
69
+ def build_store
70
+ case configuration.store.to_s
71
+ when 'file'
72
+ Stores::FileStore.new(path: configuration.store_path, run_id: configuration.run_id)
73
+ else
74
+ Stores::Memory.new
75
+ end
57
76
  end
58
77
  end
59
78
  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.1
4
+ version: 0.2.2
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