after_migrate 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +29 -0
- data/Gemfile +13 -0
- data/README.md +97 -0
- data/Rakefile +8 -0
- data/after_migrate.gemspec +47 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/after_migrate/adapters/mysql.rb +30 -0
- data/lib/after_migrate/adapters/postgresql.rb +85 -0
- data/lib/after_migrate/adapters/sqlite.rb +32 -0
- data/lib/after_migrate/collector.rb +40 -0
- data/lib/after_migrate/current.rb +13 -0
- data/lib/after_migrate/executor.rb +80 -0
- data/lib/after_migrate/railtie.rb +33 -0
- data/lib/after_migrate/version.rb +5 -0
- data/lib/after_migrate.rb +35 -0
- data/logo.png +0 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a04f2af70c48d8299c3c1eedce77608a6a57aca044711bbc2c0e8030299d18e3
|
|
4
|
+
data.tar.gz: d511331553aacbe753d40b2004af8d523b2c823ef4ba425f8491eb8a0336d234
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c3f07535162f76e33ff8a7a399bbdee3b45d534b48a5609c600504a1f8b0e9c8d4d2e027ec653b7106c12e6663f91194370a9617912bd35860cdb77e6c451e6d
|
|
7
|
+
data.tar.gz: 8c529f7d643720957caf72ae2607055e286dbcafc7c15004a03e3908a6be9eab15c7dcc9c66b83c1b741265c70a2a1476e440846178bea2799a4884857639187
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.7
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2025-04-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Smart table detection** – automatically identifies tables touched during migrations using `sql.active_record` events
|
|
9
|
+
- **PostgreSQL support**
|
|
10
|
+
- `ANALYZE` on affected tables (default) or all tables
|
|
11
|
+
- Optional `VACUUM` via config
|
|
12
|
+
- **SQLite support**
|
|
13
|
+
- `PRAGMA optimize` (SQLite 3.35+)
|
|
14
|
+
- Fallback to `VACUUM` + `ANALYZE` on older versions
|
|
15
|
+
- **MySQL support** (MySQL 5.6+ / MariaDB)
|
|
16
|
+
- `ANALYZE TABLE` on affected tables
|
|
17
|
+
- **Zero false positives** – bulletproof SQL parser ignores:
|
|
18
|
+
- Views, materialized views, functions
|
|
19
|
+
- Column names, joins, CTEs
|
|
20
|
+
- System schemas (`pg_catalog`, `information_schema`)
|
|
21
|
+
- **Configurable via environment variables or block DSL**:
|
|
22
|
+
```ruby
|
|
23
|
+
AfterMigrate.configure do |config|
|
|
24
|
+
config.enabled = false # default: true
|
|
25
|
+
config.verbose = false # default: true
|
|
26
|
+
config.vacuum = false # default: true
|
|
27
|
+
config.analyze = 'none' # only_affected_tables (default) or 'all_tables' or 'none'
|
|
28
|
+
end
|
|
29
|
+
```
|
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
|
6
|
+
|
|
7
|
+
# Specify your gem's dependencies in after_migrate.gemspec
|
|
8
|
+
gemspec
|
|
9
|
+
|
|
10
|
+
gem 'bundler', '~> 4'
|
|
11
|
+
gem 'rake', '~> 13.0'
|
|
12
|
+
gem 'rspec', '~> 3.0'
|
|
13
|
+
gem 'rubocop', '~> 1.81'
|
data/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# AfterMigrate
|
|
2
|
+
|
|
3
|
+
**Automatically run database maintenance after Rails migrations**
|
|
4
|
+
|
|
5
|
+
`after_migrate` detects tables touched during `rails db:migrate` (or related tasks) and runs the appropriate optimizer commands:
|
|
6
|
+
|
|
7
|
+
- **PostgreSQL** → `ANALYZE` (affected tables or all) + optional `VACUUM`
|
|
8
|
+
- **SQLite** → `PRAGMA optimize` (or `VACUUM` + `ANALYZE`)
|
|
9
|
+
- **MySQL** → `ANALYZE TABLE`
|
|
10
|
+
|
|
11
|
+
Stale statistics and fragmentation after schema changes silently hurt query performance.
|
|
12
|
+
`after_migrate` fixes it - automatically and precisely.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
> **Because every migration deserves a cleanup.**
|
|
17
|
+
|
|
18
|
+
[](https://badge.fury.io/rb/after_migrate)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## ✨ Features
|
|
23
|
+
|
|
24
|
+
- Smart detection of affected tables (CREATE/ALTER/INSERT/UPDATE/DELETE/etc.)
|
|
25
|
+
- Zero false positives - ignores views, columns, system tables, and complex joins
|
|
26
|
+
- Configurable via environment variables or initializer block
|
|
27
|
+
- Supports `db:migrate`, `db:rollback`, `db:migrate:redo`
|
|
28
|
+
- No monkey-patching of ActiveRecord core classes
|
|
29
|
+
- Works in development, test, CI, and production
|
|
30
|
+
- Rails 7.0+ / Ruby 3.2+ only
|
|
31
|
+
- Dependency-free
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 📦 Installation
|
|
36
|
+
|
|
37
|
+
Add to your Gemfile:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
gem 'after_migrate'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then run:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bundle install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 🚀 Usage
|
|
52
|
+
|
|
53
|
+
The gem activates automatically. No code required for default behavior.
|
|
54
|
+
|
|
55
|
+
### Default behavior (recommended)
|
|
56
|
+
|
|
57
|
+
Out of the box, it runs `ANALYZE` on **only the tables touched** during the migration (PostgreSQL default).
|
|
58
|
+
|
|
59
|
+
### Configuration
|
|
60
|
+
|
|
61
|
+
Create `config/initializers/after_migrate.rb`:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
AfterMigrate.configure do |config|
|
|
65
|
+
# Enable/disable the gem
|
|
66
|
+
config.enabled = true
|
|
67
|
+
|
|
68
|
+
# Log what’s happening
|
|
69
|
+
config.verbose = true
|
|
70
|
+
|
|
71
|
+
# Run VACUUM on affected tables (PostgreSQL only)
|
|
72
|
+
config.vacuum = false
|
|
73
|
+
|
|
74
|
+
# Choose ANALYZE strategy
|
|
75
|
+
# "only_affected_tables" - default, precise
|
|
76
|
+
# "all_tables" - full database analyze
|
|
77
|
+
# "none" - skip ANALYZE entirely
|
|
78
|
+
config.analyze = "only_affected_tables"
|
|
79
|
+
|
|
80
|
+
# Enhance rake tasks (runs maintenance after db:migrate etc.)
|
|
81
|
+
# Set to false in test env if needed
|
|
82
|
+
config.rake_tasks_enhanced = true
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 🤝 Contributing
|
|
88
|
+
|
|
89
|
+
Bug reports, feature requests, and pull requests are very welcome!
|
|
90
|
+
|
|
91
|
+
https://github.com/moskvin/after_migrate
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 📝 License
|
|
96
|
+
|
|
97
|
+
This project is available under the MIT License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'after_migrate/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'after_migrate'
|
|
9
|
+
spec.version = AfterMigrate::VERSION
|
|
10
|
+
spec.authors = ['Nikolay Moskvin']
|
|
11
|
+
spec.email = ['nikolay.moskvin@gmail.com']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'Automatically ANALYZE and VACUUM tables touched during Rails migrations.'
|
|
14
|
+
spec.description = <<~DESC
|
|
15
|
+
Runs database maintenance (ANALYZE, VACUUM, PRAGMA optimize) on exactly the tables
|
|
16
|
+
created or modified during `rails db:migrate`. Keeps query planner statistics fresh
|
|
17
|
+
and prevents fragmentation after every schema change - automatically.
|
|
18
|
+
DESC
|
|
19
|
+
spec.homepage = 'https://github.com/moskvin/after_migrate'
|
|
20
|
+
|
|
21
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
|
22
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
|
23
|
+
if spec.respond_to?(:metadata)
|
|
24
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
25
|
+
|
|
26
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
27
|
+
spec.metadata['source_code_uri'] = 'https://github.com/moskvin/after_migrate'
|
|
28
|
+
spec.metadata['changelog_uri'] = 'https://github.com/moskvin/after_migrate/blob/master/CHANGELOG.md'
|
|
29
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
30
|
+
else
|
|
31
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
|
32
|
+
'public gem pushes.'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Specify which files should be added to the gem when it is released.
|
|
36
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
37
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
38
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
39
|
+
end
|
|
40
|
+
spec.bindir = 'exe'
|
|
41
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
42
|
+
spec.require_paths = ['lib']
|
|
43
|
+
|
|
44
|
+
spec.required_ruby_version = '>= 3.2'
|
|
45
|
+
|
|
46
|
+
spec.add_dependency 'rails', '>= 7.0'
|
|
47
|
+
end
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'after_migrate'
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require 'irb'
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AfterMigrate
|
|
4
|
+
module Mysql
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def optimize_tables(connection:, table_names:, **)
|
|
8
|
+
table_names.each do |t|
|
|
9
|
+
quoted = connection.quote_table_name(t)
|
|
10
|
+
AfterMigrate.log("ANALYZE TABLE #{quoted}")
|
|
11
|
+
connection.execute("ANALYZE TABLE #{quoted}")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all_tables(schema: nil)
|
|
16
|
+
connection = ActiveRecord::Base.connection
|
|
17
|
+
database = schema.to_s.presence || connection.current_database
|
|
18
|
+
|
|
19
|
+
connection.select_values(<<~SQL.squish)
|
|
20
|
+
SELECT table_name
|
|
21
|
+
FROM information_schema.tables
|
|
22
|
+
WHERE table_schema = #{connection.quote(database)}
|
|
23
|
+
AND table_type = 'BASE TABLE' -- exclude views
|
|
24
|
+
AND table_name NOT LIKE 'ar_internal_metadata'
|
|
25
|
+
AND table_name NOT LIKE 'schema_migrations'
|
|
26
|
+
ORDER BY table_name
|
|
27
|
+
SQL
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AfterMigrate
|
|
4
|
+
module Postgresql
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def vacuum(table_name, schema: nil, verbose: true)
|
|
8
|
+
table = ActiveRecord::Base.connection.quote_table_name("#{schema}.#{table_name}")
|
|
9
|
+
query = <<~SQL.squish
|
|
10
|
+
VACUUM (#{'VERBOSE, ' if verbose}ANALYZE, INDEX_CLEANUP ON) #{table};
|
|
11
|
+
SQL
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Base.connection.execute(query)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def dead_tuples(schema: nil, table: nil, sort: nil)
|
|
17
|
+
allowed_sorts = %w[schemaname relname n_dead_tup n_live_tup dead_tuple_ratio autovacuum_count]
|
|
18
|
+
sort = 'dead_tuple_ratio' unless allowed_sorts.include?(sort)
|
|
19
|
+
query = <<~SQL.squish
|
|
20
|
+
SELECT
|
|
21
|
+
schemaname,
|
|
22
|
+
relname,
|
|
23
|
+
last_vacuum,
|
|
24
|
+
last_autovacuum,
|
|
25
|
+
vacuum_count,
|
|
26
|
+
autovacuum_count,
|
|
27
|
+
n_dead_tup,
|
|
28
|
+
n_live_tup,
|
|
29
|
+
(COALESCE(n_dead_tup, 0)::numeric / GREATEST(COALESCE(n_live_tup, 0) + COALESCE(n_dead_tup, 0), 1)::numeric) AS dead_tuple_ratio
|
|
30
|
+
FROM pg_stat_all_tables
|
|
31
|
+
WHERE COALESCE(n_dead_tup, 0) > 0
|
|
32
|
+
#{"AND schemaname = #{ActiveRecord::Base.connection.quote(schema)}" if schema}
|
|
33
|
+
#{"AND relname = #{ActiveRecord::Base.connection.quote(table)}" if table}
|
|
34
|
+
ORDER BY #{sort} DESC NULLS LAST;
|
|
35
|
+
SQL
|
|
36
|
+
|
|
37
|
+
ActiveRecord::Base.connection.execute(query)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run_vacuum(schema:)
|
|
41
|
+
tables_with_dead_tuples = dead_tuples(schema:).pluck('relname')
|
|
42
|
+
AfterMigrate.log("Vacuuming #{tables_with_dead_tuples.size} tables in schema #{schema}...")
|
|
43
|
+
tables_with_dead_tuples.each { |t| vacuum(t) }
|
|
44
|
+
tables_with_dead_tuples
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run_analyze(schema:, tables:)
|
|
48
|
+
connection = ActiveRecord::Base.connection
|
|
49
|
+
tables.each do |t|
|
|
50
|
+
table = if t.include?('.')
|
|
51
|
+
connection.quote_table_name(t)
|
|
52
|
+
else
|
|
53
|
+
connection.quote_table_name("#{schema}.#{t}")
|
|
54
|
+
end
|
|
55
|
+
AfterMigrate.log("ANALYZE VERBOSE #{table}")
|
|
56
|
+
connection.execute("ANALYZE#{AfterMigrate.configuration.verbose ? ' VERBOSE ' : ' '}#{table}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def optimize_tables(table_names:, schema:, **)
|
|
61
|
+
cleaned_tables = []
|
|
62
|
+
cleaned_tables = run_vacuum(schema:) if AfterMigrate.configuration.vacuum
|
|
63
|
+
|
|
64
|
+
tables = table_names - cleaned_tables
|
|
65
|
+
run_analyze(schema:, tables:)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def all_tables(schema: nil)
|
|
70
|
+
connection = ActiveRecord::Base.connection
|
|
71
|
+
schema_value = schema ? schema.to_s.strip : 'public'
|
|
72
|
+
|
|
73
|
+
query = <<~SQL.squish
|
|
74
|
+
SELECT c.relname AS table_name
|
|
75
|
+
FROM pg_class c
|
|
76
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
77
|
+
WHERE n.nspname = #{connection.quote(schema_value)}
|
|
78
|
+
AND c.relkind IN ('r', 'p') -- ordinary tables + partitioned tables
|
|
79
|
+
AND c.relispartition = FALSE -- exclude partition child tables
|
|
80
|
+
ORDER BY table_name
|
|
81
|
+
SQL
|
|
82
|
+
|
|
83
|
+
connection.select_values(query)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AfterMigrate
|
|
4
|
+
module Sqlite
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def optimize_tables(connection:, **)
|
|
8
|
+
version = connection.respond_to?(:sqlite_version) ? connection.sqlite_version : '0'
|
|
9
|
+
if Gem::Version.new(version.split.first || '0') >= Gem::Version.new('3.35.0')
|
|
10
|
+
AfterMigrate.log('Running PRAGMA optimize')
|
|
11
|
+
connection.execute('PRAGMA optimize;')
|
|
12
|
+
else
|
|
13
|
+
AfterMigrate.log('Running VACUUM; ANALYZE;')
|
|
14
|
+
connection.execute('VACUUM;')
|
|
15
|
+
connection.execute('ANALYZE;')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def all_tables(**)
|
|
20
|
+
# SQLite has no concept of schema (everything is in one file)
|
|
21
|
+
# The `schema:` parameter is ignored — there's only one database
|
|
22
|
+
ActiveRecord::Base.connection.select_values(<<~SQL.squish)
|
|
23
|
+
SELECT name
|
|
24
|
+
FROM sqlite_master
|
|
25
|
+
WHERE type = 'table'
|
|
26
|
+
AND name NOT LIKE 'sqlite_%'
|
|
27
|
+
AND name NOT IN ('ar_internal_metadata', 'schema_migrations')
|
|
28
|
+
ORDER BY name
|
|
29
|
+
SQL
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AfterMigrate
|
|
4
|
+
module Collector
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def call(*)
|
|
10
|
+
event = ActiveSupport::Notifications::Event.new(*)
|
|
11
|
+
sql = event.payload[:sql]&.strip
|
|
12
|
+
return unless sql
|
|
13
|
+
return unless sql.match?(/\A\s*(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|RENAME\s+TABLE|TRUNCATE)/i)
|
|
14
|
+
|
|
15
|
+
table_names = sql.scan(/(?:from|join|update|into|table)\s+((?:"\w+"|\w+)(?:\.(?:"\w+"|\w+))*)/i).flatten
|
|
16
|
+
schema ||= fetch_schema
|
|
17
|
+
# AfterMigrate.log("[#{schema}] Detected change from '#{sql}' to table: #{table_names}") if table_names.present?
|
|
18
|
+
collect_tables(schema:, table_names:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
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
|
+
def fetch_schema
|
|
31
|
+
connection = ActiveRecord::Base.connection
|
|
32
|
+
adapter = connection.adapter_name
|
|
33
|
+
case adapter
|
|
34
|
+
when 'PostgreSQL'
|
|
35
|
+
quoted = connection.schema_search_path.split(',').first
|
|
36
|
+
quoted&.delete('"')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'after_migrate/adapters/mysql'
|
|
4
|
+
require 'after_migrate/adapters/postgresql'
|
|
5
|
+
require 'after_migrate/adapters/sqlite'
|
|
6
|
+
|
|
7
|
+
module AfterMigrate
|
|
8
|
+
module Executor
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(reset: true, schema: nil)
|
|
14
|
+
# AfterMigrate.log("Executing schema: #{schema} -> #{target_tables}...")
|
|
15
|
+
return if target_tables.blank?
|
|
16
|
+
return run_optimize(schema:, tables: target_tables[schema]) if schema.present?
|
|
17
|
+
|
|
18
|
+
target_tables.each do |s, tables|
|
|
19
|
+
run_optimize(schema: s, tables:)
|
|
20
|
+
end
|
|
21
|
+
ensure
|
|
22
|
+
AfterMigrate::Current.reset if reset
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
public :call
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def run_optimize(schema:, tables:)
|
|
30
|
+
table_names = tables.to_a.sort
|
|
31
|
+
return if table_names.empty?
|
|
32
|
+
|
|
33
|
+
AfterMigrate.log("Migration touched #{table_names.size} table(s): #{table_names.join(', ')}")
|
|
34
|
+
optimize_tables(schema:, table_names:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def optimize_tables(schema:, table_names:)
|
|
38
|
+
connection = ActiveRecord::Base.connection
|
|
39
|
+
adapter = connection.adapter_name
|
|
40
|
+
case adapter
|
|
41
|
+
when 'PostgreSQL'
|
|
42
|
+
AfterMigrate::Postgresql.optimize_tables(schema:, table_names:, connection:)
|
|
43
|
+
when 'SQLite'
|
|
44
|
+
AfterMigrate::Sqlite.optimize_tables(schema:, table_names:, connection:)
|
|
45
|
+
when 'Mysql2', 'Trilogy'
|
|
46
|
+
AfterMigrate::Mysql.optimize_tables(schema:, table_names:, connection:)
|
|
47
|
+
else
|
|
48
|
+
AfterMigrate.log("No maintenance implemented for #{adapter}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def target_tables
|
|
53
|
+
case AfterMigrate.configuration.analyze
|
|
54
|
+
when 'all_tables'
|
|
55
|
+
AfterMigrate::Current.affected_tables.each_value do |schema|
|
|
56
|
+
all_tables(schema:)
|
|
57
|
+
end
|
|
58
|
+
when 'only_affected_tables'
|
|
59
|
+
AfterMigrate::Current.affected_tables
|
|
60
|
+
else
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def all_tables(schema:)
|
|
66
|
+
connection = ActiveRecord::Base.connection
|
|
67
|
+
adapter = connection.adapter_name
|
|
68
|
+
case adapter
|
|
69
|
+
when 'PostgreSQL'
|
|
70
|
+
AfterMigrate::Postgresql.all_tables(schema:)
|
|
71
|
+
when 'SQLite'
|
|
72
|
+
AfterMigrate::Sqlite.all_tables(schema:)
|
|
73
|
+
when 'Mysql2', 'Trilogy'
|
|
74
|
+
AfterMigrate::Mysql.all_tables(schema:)
|
|
75
|
+
else
|
|
76
|
+
[]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails'
|
|
4
|
+
require 'active_support/notifications'
|
|
5
|
+
|
|
6
|
+
module AfterMigrate
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
initializer 'after_migrate.subscribe' do |app|
|
|
9
|
+
next unless AfterMigrate.configuration.enabled
|
|
10
|
+
|
|
11
|
+
subscription = ActiveSupport::Notifications.subscribe 'sql.active_record' do |*args|
|
|
12
|
+
AfterMigrate::Collector.call(*args)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
app.executor.to_run { ActiveSupport::Notifications.unsubscribe(subscription) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
rake_tasks do
|
|
19
|
+
%w[
|
|
20
|
+
db:migrate
|
|
21
|
+
db:migrate:up
|
|
22
|
+
db:migrate:redo
|
|
23
|
+
].each do |task_name|
|
|
24
|
+
Rake::Task[task_name].enhance do
|
|
25
|
+
next unless AfterMigrate.configuration.enabled
|
|
26
|
+
next unless AfterMigrate.configuration.rake_tasks_enhanced
|
|
27
|
+
|
|
28
|
+
AfterMigrate::Executor.call(reset: true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'after_migrate/version'
|
|
4
|
+
require 'after_migrate/current'
|
|
5
|
+
require 'after_migrate/collector'
|
|
6
|
+
require 'after_migrate/executor'
|
|
7
|
+
require 'after_migrate/railtie'
|
|
8
|
+
|
|
9
|
+
module AfterMigrate
|
|
10
|
+
class Configuration
|
|
11
|
+
attr_accessor :enabled, :verbose, :vacuum, :analyze, :rake_tasks_enhanced
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@enabled = true
|
|
15
|
+
@verbose = true
|
|
16
|
+
@vacuum = true
|
|
17
|
+
@analyze = 'only_affected_tables'
|
|
18
|
+
@rake_tasks_enhanced = true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def configuration
|
|
24
|
+
@configuration ||= Configuration.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configure
|
|
28
|
+
yield(configuration)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def log(msg)
|
|
32
|
+
warn "[after_migrate] #{msg}" if AfterMigrate.configuration.verbose
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/logo.png
ADDED
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: after_migrate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nikolay Moskvin
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
description: |
|
|
27
|
+
Runs database maintenance (ANALYZE, VACUUM, PRAGMA optimize) on exactly the tables
|
|
28
|
+
created or modified during `rails db:migrate`. Keeps query planner statistics fresh
|
|
29
|
+
and prevents fragmentation after every schema change - automatically.
|
|
30
|
+
email:
|
|
31
|
+
- nikolay.moskvin@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- ".gitignore"
|
|
37
|
+
- ".rspec"
|
|
38
|
+
- ".rubocop.yml"
|
|
39
|
+
- ".ruby-version"
|
|
40
|
+
- ".travis.yml"
|
|
41
|
+
- CHANGELOG.md
|
|
42
|
+
- Gemfile
|
|
43
|
+
- README.md
|
|
44
|
+
- Rakefile
|
|
45
|
+
- after_migrate.gemspec
|
|
46
|
+
- bin/console
|
|
47
|
+
- bin/setup
|
|
48
|
+
- lib/after_migrate.rb
|
|
49
|
+
- lib/after_migrate/adapters/mysql.rb
|
|
50
|
+
- lib/after_migrate/adapters/postgresql.rb
|
|
51
|
+
- lib/after_migrate/adapters/sqlite.rb
|
|
52
|
+
- lib/after_migrate/collector.rb
|
|
53
|
+
- lib/after_migrate/current.rb
|
|
54
|
+
- lib/after_migrate/executor.rb
|
|
55
|
+
- lib/after_migrate/railtie.rb
|
|
56
|
+
- lib/after_migrate/version.rb
|
|
57
|
+
- logo.png
|
|
58
|
+
homepage: https://github.com/moskvin/after_migrate
|
|
59
|
+
licenses: []
|
|
60
|
+
metadata:
|
|
61
|
+
allowed_push_host: https://rubygems.org
|
|
62
|
+
homepage_uri: https://github.com/moskvin/after_migrate
|
|
63
|
+
source_code_uri: https://github.com/moskvin/after_migrate
|
|
64
|
+
changelog_uri: https://github.com/moskvin/after_migrate/blob/master/CHANGELOG.md
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '3.2'
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.6.9
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Automatically ANALYZE and VACUUM tables touched during Rails migrations.
|
|
83
|
+
test_files: []
|