after_migrate 0.1.1 → 0.2.1

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: 2602aa9ee3d2ffa2c1ea85ca8cbfa1b101679aa42ed05e0a4cfa4c742f258f59
4
- data.tar.gz: d581242202742eb6a62be1cd1799a49c196ca82c99e8b36428ee6390fd12c415
3
+ metadata.gz: 19aeb8ce5d84227e4ea467f7e0d14ff8ccdb34f49214f9201b7550ede7ffbbf3
4
+ data.tar.gz: 2c52a7d680cb37bc2536be8ccd822fee3b92fc6e2b91a5587fa7da1826d14993
5
5
  SHA512:
6
- metadata.gz: d81dce8555f469b190b3cfd37f7eb2b919aab714e6fcfb1842271c323495b213be2c2e779c8400ad67d28832c11dbc7ad7147e90f93b6950069b6b6b9ea8d558
7
- data.tar.gz: 77f23e51b264cf80b5cbf028a0e43106d13498f9f7d109b1eafe26c6380269c08bb3574946ec534d08b8ac175a09c008823e64d0c862c4e4441e66c1df145399
6
+ metadata.gz: f8f671d511d7dc8cecc9a33e8bd90752cd0bb5b5ccb50a485dd7215c50ee68d76ec9ce626850d18717bb507a50e0bfe31e71425d24975607144b3749da1d9345
7
+ data.tar.gz: 80e42d103e33f52644366ebd660a0d5252c55934386830c68f33c9418b37ffbb92310c1c2dc2dd033d9a62204f0bd8d8c78f222852796d5838c4526ae313809e
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ .idea/
13
+ /Gemfile.lock
data/.rubocop.yml CHANGED
@@ -1,2 +1,14 @@
1
1
  AllCops:
2
2
  NewCops: enable
3
+ SuggestExtensions: false
4
+
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ Metrics/MethodLength:
9
+ Max: 20
10
+
11
+ Metrics/BlockLength:
12
+ Exclude:
13
+ - 'after_migrate.gemspec'
14
+ - 'spec/**/*.rb'
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.7
1
+ 3.4.9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.0] - 2026-04-28
6
+
7
+ ### Added
8
+ - `defer` configuration option (default: `true`) — when enabled, rake task enhancements only collect touched tables; they do not run maintenance automatically
9
+ - `AfterMigrate.run!` public API to explicitly trigger maintenance on all collected tables across all schemas, then clear the store — intended to be called once after all tenant migrations complete in multi-tenant setups
10
+ - `after_migrate:run` rake task as a convenience wrapper around `AfterMigrate.run!`
11
+
12
+ ### Changed
13
+ - **Breaking**: replaced `AfterMigrate::Current` (`ActiveSupport::CurrentAttributes`) with a module-level persistent store (`Concurrent::Map`). The store accumulates touched tables across multiple rake task invocations and only resets when `AfterMigrate.run!` is called.
14
+ - Fixed `all_tables` analyze mode - previously iterated `.each_value` (yielding `Concurrent::Set` objects) instead of schema names, producing incorrect SQL queries.
15
+
16
+ ### Removed
17
+ - `AfterMigrate::Current` - no longer needed. If you referenced this class directly, use `AfterMigrate.affected_tables` instead.
18
+ - `Executor.call(reset:)` parameter - the store is always cleared after `run!`; pass `schema:` to scope execution to a single schema.
19
+
5
20
  ## [0.1.0] - 2025-04-05
6
21
 
7
22
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,74 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ bundle install # install dependencies
9
+ bundle exec rspec # run all tests
10
+ bundle exec rspec spec/after_migrate_spec.rb # run a single spec file
11
+ bundle exec rake # default task (runs spec)
12
+ bundle exec rubocop # lint
13
+ bundle exec rubocop -a # lint with auto-fix
14
+ rake release # build and push gem to RubyGems
15
+ ```
16
+
17
+ ## Architecture
18
+
19
+ This is a Rails gem that automatically runs database maintenance (`ANALYZE`, `VACUUM`, `PRAGMA optimize`) after `db:migrate` tasks. The core flow is:
20
+
21
+ 1. **`Railtie`** (`lib/after_migrate/railtie.rb`) — the entry point. On Rails init, it subscribes to `sql.active_record` notifications so every SQL statement during a migration is intercepted. It enhances `db:migrate`, `db:migrate:up`, and `db:migrate:redo` to call `AfterMigrate.run!` when they complete — unless `defer: true` (the default), in which case the rake task only collects. It also registers the `after_migrate:run` task.
22
+
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
+
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.
26
+
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
+
29
+ 5. **Adapters** (`lib/after_migrate/adapters/`) — one module per database:
30
+ - `Sql` — shared regex-based table parser (used by MySQL and SQLite, which can't use pg_query)
31
+ - `Postgresql` — uses `pg_query` gem for accurate SQL parsing; runs `VACUUM` (checking `pg_stat_all_tables` for dead tuples) then `ANALYZE VERBOSE` per table
32
+ - `Mysql` — runs `ANALYZE TABLE` per table; lists tables from `information_schema`
33
+ - `Sqlite` — runs `PRAGMA optimize` (SQLite ≥ 3.35.0) or `VACUUM; ANALYZE;`
34
+
35
+ ## Key design decisions
36
+
37
+ - PostgreSQL uses `pg_query` (the actual Postgres parser) for table extraction, which avoids false positives from regex. MySQL and SQLite fall back to the shared `Sql` regex patterns.
38
+ - `pg_query` is a hard runtime dependency (listed in gemspec), not optional — even though it's only used for PostgreSQL.
39
+ - Table collection uses `Concurrent::Map` + `Concurrent::Set` (from `concurrent-ruby`) for thread-safe accumulation across parallel migration workers.
40
+ - The gem does **not** monkey-patch ActiveRecord. It only uses public `ActiveSupport::Notifications` and `Rake::Task#enhance` APIs.
41
+ - `defer: true` (the default) is the multi-tenant-friendly mode: rake tasks only collect, never execute. Call `AfterMigrate.run!` (or `rake after_migrate:run`) once after all tenant migrations complete.
42
+ - The `app.executor.to_run` unsubscription in `Railtie` ensures the SQL subscription is dropped before the app starts serving web requests, so normal traffic is never collected.
43
+
44
+ ## Configuration
45
+
46
+ Config values are in `AfterMigrate::Configuration` (initialized in `lib/after_migrate.rb`):
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 |
55
+ | `defer` | `true` | bool — skip auto-run; call `AfterMigrate.run!` manually |
56
+
57
+ ## Multi-tenant usage
58
+
59
+ ```ruby
60
+ # In your tenant migration runner:
61
+ Tenant.each do |tenant|
62
+ tenant.switch { ActiveRecord::MigrationContext.new(...).migrate }
63
+ end
64
+ # Tables are now accumulated per schema in AfterMigrate.affected_tables
65
+ AfterMigrate.run! # or: Rake::Task['after_migrate:run'].invoke
66
+ ```
67
+
68
+ Set `defer: false` to restore the v0.1 behaviour of running after each `db:migrate`.
69
+
70
+ ## Dependencies
71
+
72
+ - Ruby ≥ 3.2, Rails ≥ 7.0
73
+ - `pg_query ≥ 6.1` (required even for non-Postgres installs)
74
+ - Dev: `rspec ~> 3.0`, `rubocop ~> 1.81`, `bundler ~> 4`
data/README.md CHANGED
@@ -83,6 +83,30 @@ end
83
83
  ```
84
84
  ---
85
85
 
86
+ ## 🚢 Releasing
87
+
88
+ Use the guarded helper to make sure git is pushed before RubyGems publish:
89
+
90
+ ```bash
91
+ bin/release
92
+ ```
93
+
94
+ What it does:
95
+ - requires a clean git worktree
96
+ - ensures you are on the default branch (`origin/HEAD` fallback)
97
+ - checks branch sync status vs `origin`
98
+ - runs `bundle exec rspec` and `bundle exec rubocop`
99
+ - pushes the branch first
100
+ - runs `bundle exec rake release` (build/tag/push/publish)
101
+
102
+ Optional:
103
+
104
+ ```bash
105
+ bin/release --skip-checks
106
+ ```
107
+
108
+ ---
109
+
86
110
  ## 🤝 Contributing
87
111
 
88
112
  Bug reports, feature requests, and pull requests are very welcome!
data/bin/release ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ root_dir=$(cd "$(dirname "$0")/.." && pwd)
6
+ cd "$root_dir"
7
+
8
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
9
+ cat <<'USAGE'
10
+ Usage: bin/release [--skip-checks]
11
+
12
+ Runs guardrails before releasing:
13
+ 1. Requires a clean git worktree
14
+ 2. Requires current branch to match origin/HEAD (or local HEAD fallback)
15
+ 3. Requires branch to be in sync with origin
16
+ 4. Runs test + lint checks (unless --skip-checks)
17
+ 5. Pushes the branch, then runs `bundle exec rake release`
18
+ USAGE
19
+ exit 0
20
+ fi
21
+
22
+ skip_checks=false
23
+ if [[ "${1:-}" == "--skip-checks" ]]; then
24
+ skip_checks=true
25
+ elif [[ $# -gt 0 ]]; then
26
+ echo "Unknown option: $1" >&2
27
+ exit 1
28
+ fi
29
+
30
+ if [[ -n "$(git status --porcelain)" ]]; then
31
+ echo "Working tree is not clean. Commit or stash changes before releasing." >&2
32
+ exit 1
33
+ fi
34
+
35
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
36
+ default_branch=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
37
+ default_branch=${default_branch:-$current_branch}
38
+
39
+ if [[ "$current_branch" != "$default_branch" ]]; then
40
+ echo "Release must run from '$default_branch' (current: '$current_branch')." >&2
41
+ exit 1
42
+ fi
43
+
44
+ echo "Fetching latest refs from origin..."
45
+ git fetch origin
46
+
47
+ local_sha=$(git rev-parse @)
48
+ remote_sha=$(git rev-parse "origin/$default_branch")
49
+ base_sha=$(git merge-base @ "origin/$default_branch")
50
+
51
+ if [[ "$local_sha" == "$remote_sha" ]]; then
52
+ echo "Branch is in sync with origin/$default_branch."
53
+ elif [[ "$local_sha" == "$base_sha" ]]; then
54
+ echo "Local branch is behind origin/$default_branch. Pull/rebase first." >&2
55
+ exit 1
56
+ elif [[ "$remote_sha" == "$base_sha" ]]; then
57
+ echo "Local branch is ahead of origin/$default_branch and will be pushed."
58
+ else
59
+ echo "Local and origin/$default_branch have diverged. Rebase/merge first." >&2
60
+ exit 1
61
+ fi
62
+
63
+ version=$(ruby -e 'require_relative "lib/after_migrate/version"; puts AfterMigrate::VERSION')
64
+ tag="v$version"
65
+
66
+ if git rev-parse "$tag" >/dev/null 2>&1 || git ls-remote --tags origin "$tag" | grep -q "$tag"; then
67
+ echo "Tag '$tag' already exists locally or on origin. Bump version first." >&2
68
+ exit 1
69
+ fi
70
+
71
+ if [[ "$skip_checks" == "false" ]]; then
72
+ echo "Running specs..."
73
+ bundle exec rspec
74
+
75
+ echo "Running RuboCop..."
76
+ bundle exec rubocop
77
+ fi
78
+
79
+ echo "Pushing branch '$default_branch' first..."
80
+ git push origin "$default_branch"
81
+
82
+ echo "Releasing gem with rake task..."
83
+ bundle exec rake release
84
+
85
+ echo "Release complete: $tag"
@@ -9,7 +9,8 @@ module AfterMigrate
9
9
  module_function
10
10
 
11
11
  def vacuum(table_name, schema: nil, verbose: true)
12
- table = ActiveRecord::Base.connection.quote_table_name("#{schema}.#{table_name}")
12
+ qualified = schema.present? ? "#{schema}.#{table_name}" : table_name
13
+ table = ActiveRecord::Base.connection.quote_table_name(qualified)
13
14
  query = <<~SQL.squish
14
15
  VACUUM (#{'VERBOSE, ' if verbose}ANALYZE, INDEX_CLEANUP ON) #{table};
15
16
  SQL
@@ -41,10 +42,11 @@ module AfterMigrate
41
42
  ActiveRecord::Base.connection.execute(query)
42
43
  end
43
44
 
44
- def run_vacuum(schema:)
45
+ def run_vacuum(schema:, table_names: nil)
45
46
  tables_with_dead_tuples = dead_tuples(schema:).pluck('relname')
47
+ tables_with_dead_tuples &= Array(table_names) if table_names
46
48
  AfterMigrate.log("Vacuuming #{tables_with_dead_tuples.size} tables in schema #{schema}...")
47
- tables_with_dead_tuples.each { |t| vacuum(t) }
49
+ tables_with_dead_tuples.each { |t| vacuum(t, schema:, verbose: AfterMigrate.configuration.verbose) }
48
50
  tables_with_dead_tuples
49
51
  end
50
52
 
@@ -63,7 +65,9 @@ module AfterMigrate
63
65
 
64
66
  def optimize_tables(table_names:, schema:, **)
65
67
  cleaned_tables = []
66
- cleaned_tables = run_vacuum(schema:) if AfterMigrate.configuration.vacuum
68
+ cleaned_tables = run_vacuum(schema:, table_names:) if AfterMigrate.configuration.vacuum
69
+
70
+ return if AfterMigrate.configuration.analyze == 'none'
67
71
 
68
72
  tables = table_names - cleaned_tables
69
73
  run_analyze(schema:, tables:)
@@ -3,18 +3,18 @@
3
3
  module AfterMigrate
4
4
  module Sql
5
5
  IDENT = /
6
- (?:"[\w]+"|\w+)
7
- (?:\.(?:"[\w]+"|\w+))*
6
+ (?:"\w+"|\w+)
7
+ (?:\.(?:"\w+"|\w+))*
8
8
  /x
9
9
 
10
10
  PATTERNS = {
11
- update: /update\s+(?:only\s+)?(#{IDENT})(?!\s*\()/ix,
12
- insert: /insert\s+into\s+(#{IDENT})(?!\s*\()/ix,
13
- delete: /delete\s+from\s+(#{IDENT})(?!\s*\()/ix,
14
- drop_table: /drop\s+table\s+(?:if\s+exists\s+)?(#{IDENT})(?!\s*\()/ix,
15
- alter_table: /alter\s+table\s+(#{IDENT})(?!\s*\()/ix,
16
- create_table: /create\s+table\s+(?:if\s+not\s+exists\s+)?(#{IDENT})(?!\s*\()/ix,
17
- from_join: /(?:from|join)\s+(#{IDENT})(?!\s*\()/ix
11
+ update: /update\s+(?:only\s+)?(#{IDENT})(?!\()/ix,
12
+ insert: /insert\s+into\s+(#{IDENT})(?!\()/ix,
13
+ delete: /delete\s+from\s+(#{IDENT})(?!\()/ix,
14
+ drop_table: /drop\s+table\s+(?:if\s+exists\s+)?(#{IDENT})(?!\()/ix,
15
+ alter_table: /alter\s+table\s+(#{IDENT})(?!\()/ix,
16
+ create_table: /create\s+table\s+(?:if\s+not\s+exists\s+)?(#{IDENT})(?!\()/ix,
17
+ from_join: /(?:from|join)\s+(#{IDENT})(?!\()/ix
18
18
  }.freeze
19
19
 
20
20
  def parse_tables(sql)
@@ -13,24 +13,15 @@ module AfterMigrate
13
13
  return unless sql.match?(/\A\s*(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|RENAME\s+TABLE|TRUNCATE)/i)
14
14
 
15
15
  table_names = parse_tables(sql)
16
- schema ||= fetch_schema
17
- # AfterMigrate.log("[#{schema}] Detected change from '#{sql}' to tables: #{table_names}") if table_names.present?
18
- collect_tables(schema:, table_names:)
16
+ schema = fetch_schema
17
+ AfterMigrate.merge_tables(schema, table_names)
19
18
  end
20
19
 
21
20
  private
22
21
 
23
- def collect_tables(schema:, table_names:)
24
- return if table_names.blank?
25
-
26
- AfterMigrate::Current.affected_tables ||= Hash.new { |h, k| h[k] = Concurrent::Set.new }
27
- AfterMigrate::Current.affected_tables[schema].merge(table_names)
28
- end
29
-
30
22
  def fetch_schema
31
23
  connection = ActiveRecord::Base.connection
32
- adapter = connection.adapter_name
33
- case adapter
24
+ case connection.adapter_name
34
25
  when 'PostgreSQL'
35
26
  quoted = connection.schema_search_path.split(',').first
36
27
  quoted&.delete('"')
@@ -39,8 +30,7 @@ module AfterMigrate
39
30
 
40
31
  def parse_tables(sql)
41
32
  connection = ActiveRecord::Base.connection
42
- adapter = connection.adapter_name
43
- case adapter
33
+ case connection.adapter_name
44
34
  when 'PostgreSQL'
45
35
  AfterMigrate::Postgresql.parse_tables(sql)
46
36
  when 'SQLite'
@@ -48,7 +38,7 @@ module AfterMigrate
48
38
  when 'Mysql2', 'Trilogy'
49
39
  AfterMigrate::Mysql.parse_tables(sql)
50
40
  else
51
- AfterMigrate.log("No maintenance implemented for #{adapter}")
41
+ AfterMigrate.log("No maintenance implemented for #{connection.adapter_name}")
52
42
  end
53
43
  end
54
44
  end
@@ -11,16 +11,19 @@ module AfterMigrate
11
11
 
12
12
  module_function
13
13
 
14
- def call(reset: true, schema: nil)
15
- # AfterMigrate.log("Executing schema: #{schema} -> #{target_tables}...")
16
- return if target_tables.blank?
17
- return run_optimize(schema:, tables: target_tables[schema]) if schema.present?
14
+ def call(schema: nil)
15
+ return unless AfterMigrate.configuration.vacuum || AfterMigrate.configuration.analyze != 'none'
18
16
 
19
- target_tables.each do |s, tables|
20
- run_optimize(schema: s, tables:)
17
+ tables = target_tables
18
+ return if tables.blank?
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) }
21
24
  end
22
25
  ensure
23
- AfterMigrate::Current.reset if reset
26
+ AfterMigrate.reset!
24
27
  end
25
28
 
26
29
  public :call
@@ -31,14 +34,14 @@ module AfterMigrate
31
34
  table_names = tables.to_a.sort
32
35
  return if table_names.empty?
33
36
 
34
- AfterMigrate.log("Migration touched #{table_names.size} table(s): #{table_names.join(', ')}")
37
+ message = "Migration touched #{table_names.size} table(s) in schema #{schema.inspect}: #{table_names.join(', ')}"
38
+ AfterMigrate.log(message)
35
39
  optimize_tables(schema:, table_names:)
36
40
  end
37
41
 
38
42
  def optimize_tables(schema:, table_names:)
39
43
  connection = ActiveRecord::Base.connection
40
- adapter = connection.adapter_name
41
- case adapter
44
+ case connection.adapter_name
42
45
  when 'PostgreSQL'
43
46
  AfterMigrate::Postgresql.optimize_tables(schema:, table_names:, connection:)
44
47
  when 'SQLite'
@@ -46,27 +49,25 @@ module AfterMigrate
46
49
  when 'Mysql2', 'Trilogy'
47
50
  AfterMigrate::Mysql.optimize_tables(schema:, table_names:, connection:)
48
51
  else
49
- AfterMigrate.log("No maintenance implemented for #{adapter}")
52
+ AfterMigrate.log("No maintenance implemented for #{connection.adapter_name}")
50
53
  end
51
54
  end
52
55
 
53
56
  def target_tables
54
57
  case AfterMigrate.configuration.analyze
55
58
  when 'all_tables'
56
- AfterMigrate::Current.affected_tables.each_value do |schema|
57
- all_tables(schema:)
59
+ AfterMigrate.affected_tables.keys.each_with_object({}) do |schema, hash|
60
+ hash[schema] = all_tables(schema:)
58
61
  end
59
- when 'only_affected_tables'
60
- AfterMigrate::Current.affected_tables
61
62
  else
62
- []
63
+ # 'only_affected_tables' or 'none' — vacuum still needs the affected list
64
+ AfterMigrate.affected_tables
63
65
  end
64
66
  end
65
67
 
66
68
  def all_tables(schema:)
67
69
  connection = ActiveRecord::Base.connection
68
- adapter = connection.adapter_name
69
- case adapter
70
+ case connection.adapter_name
70
71
  when 'PostgreSQL'
71
72
  AfterMigrate::Postgresql.all_tables(schema:)
72
73
  when 'SQLite'
@@ -12,6 +12,8 @@ module AfterMigrate
12
12
  AfterMigrate::Collector.call(*args)
13
13
  end
14
14
 
15
+ # Unsubscribe when the app starts serving requests so normal web traffic
16
+ # is never collected.
15
17
  app.executor.to_run { ActiveSupport::Notifications.unsubscribe(subscription) }
16
18
  end
17
19
 
@@ -24,8 +26,16 @@ module AfterMigrate
24
26
  Rake::Task[task_name].enhance do
25
27
  next unless AfterMigrate.configuration.enabled
26
28
  next unless AfterMigrate.configuration.rake_tasks_enhanced
29
+ next if AfterMigrate.configuration.defer
27
30
 
28
- AfterMigrate::Executor.call(reset: true)
31
+ AfterMigrate.run!
32
+ end
33
+ end
34
+
35
+ namespace :after_migrate do
36
+ desc 'Run database maintenance (ANALYZE/VACUUM) on all tables collected across migrations'
37
+ task run: :environment do
38
+ AfterMigrate.run!
29
39
  end
30
40
  end
31
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AfterMigrate
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.1'
5
5
  end
data/lib/after_migrate.rb CHANGED
@@ -1,14 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'after_migrate/version'
4
- require 'after_migrate/current'
5
4
  require 'after_migrate/collector'
6
5
  require 'after_migrate/executor'
7
6
  require 'after_migrate/railtie'
8
7
 
9
8
  module AfterMigrate
10
9
  class Configuration
11
- attr_accessor :enabled, :verbose, :vacuum, :analyze, :rake_tasks_enhanced
10
+ attr_accessor :enabled, :verbose, :vacuum, :analyze, :rake_tasks_enhanced, :defer
12
11
 
13
12
  def initialize
14
13
  @enabled = true
@@ -16,6 +15,7 @@ module AfterMigrate
16
15
  @vacuum = true
17
16
  @analyze = 'only_affected_tables'
18
17
  @rake_tasks_enhanced = true
18
+ @defer = true
19
19
  end
20
20
  end
21
21
 
@@ -31,5 +31,29 @@ module AfterMigrate
31
31
  def log(msg)
32
32
  warn "[after_migrate] #{msg}" if AfterMigrate.configuration.verbose
33
33
  end
34
+
35
+ # Persistent cross-migration store: schema_name => Concurrent::Set<table_name>
36
+ def affected_tables
37
+ @affected_tables ||= Concurrent::Map.new
38
+ end
39
+
40
+ 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
+ end
46
+
47
+ # Trigger database maintenance on all collected tables, then clear the store.
48
+ # In multi-tenant setups call this once after all tenant migrations complete.
49
+ def run!(schema: nil)
50
+ return unless configuration.enabled
51
+
52
+ Executor.call(schema:)
53
+ end
54
+
55
+ def reset!
56
+ @affected_tables = nil
57
+ end
34
58
  end
35
59
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: after_migrate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikolay Moskvin
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-12-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: pg_query
@@ -54,11 +53,13 @@ files:
54
53
  - ".ruby-version"
55
54
  - ".travis.yml"
56
55
  - CHANGELOG.md
56
+ - CLAUDE.md
57
57
  - Gemfile
58
58
  - README.md
59
59
  - Rakefile
60
60
  - after_migrate.gemspec
61
61
  - bin/console
62
+ - bin/release
62
63
  - bin/setup
63
64
  - lib/after_migrate.rb
64
65
  - lib/after_migrate/adapters/mysql.rb
@@ -66,7 +67,6 @@ files:
66
67
  - lib/after_migrate/adapters/sql.rb
67
68
  - lib/after_migrate/adapters/sqlite.rb
68
69
  - lib/after_migrate/collector.rb
69
- - lib/after_migrate/current.rb
70
70
  - lib/after_migrate/executor.rb
71
71
  - lib/after_migrate/railtie.rb
72
72
  - lib/after_migrate/version.rb
@@ -79,7 +79,6 @@ metadata:
79
79
  source_code_uri: https://github.com/moskvin/after_migrate
80
80
  changelog_uri: https://github.com/moskvin/after_migrate/blob/master/CHANGELOG.md
81
81
  rubygems_mfa_required: 'true'
82
- post_install_message:
83
82
  rdoc_options: []
84
83
  require_paths:
85
84
  - lib
@@ -94,8 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
93
  - !ruby/object:Gem::Version
95
94
  version: '0'
96
95
  requirements: []
97
- rubygems_version: 3.4.19
98
- signing_key:
96
+ rubygems_version: 3.6.9
99
97
  specification_version: 4
100
98
  summary: Automatically ANALYZE and VACUUM tables touched during Rails migrations.
101
99
  test_files: []
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'active_support/current_attributes'
4
-
5
- module AfterMigrate
6
- class Current < ActiveSupport::CurrentAttributes
7
- attribute :affected_tables
8
-
9
- resets do
10
- self.affected_tables = Hash.new { |h, k| h[k] = Concurrent::Set.new }
11
- end
12
- end
13
- end