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 +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +15 -0
- data/CLAUDE.md +74 -0
- data/README.md +24 -0
- data/bin/release +85 -0
- data/lib/after_migrate/adapters/postgresql.rb +8 -4
- data/lib/after_migrate/adapters/sql.rb +9 -9
- data/lib/after_migrate/collector.rb +5 -15
- data/lib/after_migrate/executor.rb +19 -18
- data/lib/after_migrate/railtie.rb +11 -1
- data/lib/after_migrate/version.rb +1 -1
- data/lib/after_migrate.rb +26 -2
- metadata +5 -7
- data/lib/after_migrate/current.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19aeb8ce5d84227e4ea467f7e0d14ff8ccdb34f49214f9201b7550ede7ffbbf3
|
|
4
|
+
data.tar.gz: 2c52a7d680cb37bc2536be8ccd822fee3b92fc6e2b91a5587fa7da1826d14993
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8f671d511d7dc8cecc9a33e8bd90752cd0bb5b5ccb50a485dd7215c50ee68d76ec9ce626850d18717bb507a50e0bfe31e71425d24975607144b3749da1d9345
|
|
7
|
+
data.tar.gz: 80e42d103e33f52644366ebd660a0d5252c55934386830c68f33c9418b37ffbb92310c1c2dc2dd033d9a62204f0bd8d8c78f222852796d5838c4526ae313809e
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.4.
|
|
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
|
-
|
|
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
|
-
(?:"
|
|
7
|
-
(?:\.(?:"
|
|
6
|
+
(?:"\w+"|\w+)
|
|
7
|
+
(?:\.(?:"\w+"|\w+))*
|
|
8
8
|
/x
|
|
9
9
|
|
|
10
10
|
PATTERNS = {
|
|
11
|
-
update: /update\s+(?:only\s+)?(#{IDENT})(?!\
|
|
12
|
-
insert: /insert\s+into\s+(#{IDENT})(?!\
|
|
13
|
-
delete: /delete\s+from\s+(#{IDENT})(?!\
|
|
14
|
-
drop_table: /drop\s+table\s+(?:if\s+exists\s+)?(#{IDENT})(?!\
|
|
15
|
-
alter_table: /alter\s+table\s+(#{IDENT})(?!\
|
|
16
|
-
create_table: /create\s+table\s+(?:if\s+not\s+exists\s+)?(#{IDENT})(?!\
|
|
17
|
-
from_join: /(?:from|join)\s+(#{IDENT})(?!\
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 #{
|
|
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(
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 #{
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
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.
|
|
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:
|
|
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.
|
|
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
|