schema_reaper 1.0.4 → 1.0.6
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/CHANGELOG.md +28 -0
- data/lib/schema_reaper/finding.rb +13 -0
- data/lib/schema_reaper/migration_generator.rb +19 -8
- data/lib/schema_reaper/runner.rb +20 -1
- data/lib/schema_reaper/version.rb +1 -1
- data/schema_reaper.gemspec +2 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c56279086faa2b6772a769f586f78a0f3e332973b25ad2f347c7e2533740358
|
|
4
|
+
data.tar.gz: 95e06e2cbb13f0887ff773e1c0fcde931e9f97bc3c3de6a4739eecf7ff7d8990
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c235d143cc6d32736a64f9e425c8fe1ffc3f0585f55716c1284bbb6896fbcf54fbc30361fbee92c3f8e280aca9a909b6c020d3d563c0df9b8ab67653e77d5dde
|
|
7
|
+
data.tar.gz: de7f08b01afeebd5c0638ccc15f2be1a077e151b9cd69525788da091af411087716a20c8fe36dde49bfb5432bb77b5cf7b5c90827fe38ababc6e882563d91675
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.6] - 2026-09-04
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Added `mitkush` (mitanshukushwah@gmail.com) as a gemspec author.
|
|
7
|
+
|
|
8
|
+
## [1.0.5] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
Fixes found by running the gem against a live PostgreSQL database.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Duplicate findings for one object.** When several analyzers flagged the
|
|
14
|
+
same physical column (e.g. `dead_column` + `always_null_column`) or index
|
|
15
|
+
(`unused_index` + `duplicate_index`), each was reported separately *and* its
|
|
16
|
+
reclaimable bytes were counted more than once, inflating the run total.
|
|
17
|
+
Findings are now collapsed to the highest-confidence one per target; the
|
|
18
|
+
others are noted as `also flagged by: ...` in its evidence.
|
|
19
|
+
- **`generate-migration` output.** The generated STEP 1 migration contained a
|
|
20
|
+
fragile multi-line string (heredoc line-continuation) that rendered with a
|
|
21
|
+
stray gap. It is now a single clean string. Both files are verified valid
|
|
22
|
+
Ruby.
|
|
23
|
+
- **Migration schema version.** Generated migrations now inherit the host
|
|
24
|
+
app's Rails minor version (`ActiveRecord::Migration[X.Y]`) when ActiveRecord
|
|
25
|
+
is loaded, instead of a hard-coded `7.1`.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Specs covering target collapsing and migration generation, exercised end to
|
|
29
|
+
end through `Runner` with an injected schema.
|
|
30
|
+
|
|
3
31
|
## [1.0.4] - 2026-09-04
|
|
4
32
|
|
|
5
33
|
### Changed
|
|
@@ -19,6 +19,19 @@ module SchemaReaper
|
|
|
19
19
|
[type, table, column, index].compact.join("/")
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# The physical object this finding is about, ignoring which analyzer raised
|
|
23
|
+
# it. Two findings with the same target_key are the same underlying problem
|
|
24
|
+
# (e.g. dead_column + always_null_column on one column) -- keep the strongest.
|
|
25
|
+
def target_key
|
|
26
|
+
if index
|
|
27
|
+
["index", table, index]
|
|
28
|
+
elsif column
|
|
29
|
+
["column", table, column]
|
|
30
|
+
else
|
|
31
|
+
["table", table]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
22
35
|
def reclaimable_bytes
|
|
23
36
|
self[:reclaimable_bytes] || 0
|
|
24
37
|
end
|
|
@@ -19,35 +19,38 @@ module SchemaReaper
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def stage_one
|
|
22
|
+
note = "Add `self.ignored_columns += %w[#{@column}]` to the #{model} model, " \
|
|
23
|
+
"then deploy STEP 2 after a soak period."
|
|
22
24
|
write "ignore_#{@table}_#{@column}", <<~RUBY
|
|
23
25
|
# frozen_string_literal: true
|
|
24
26
|
|
|
25
27
|
# STEP 1 of 2. Deploy this alone and let it soak. It only tells
|
|
26
28
|
# ActiveRecord to stop selecting the column; nothing is dropped.
|
|
27
|
-
class Ignore#{camel}Column < ActiveRecord::Migration[
|
|
29
|
+
class Ignore#{camel}Column < ActiveRecord::Migration[#{migration_version}]
|
|
28
30
|
def up
|
|
29
|
-
say
|
|
30
|
-
"then deploy STEP 2 after a soak period."
|
|
31
|
+
say #{note.inspect}
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def down
|
|
34
|
+
def down
|
|
35
|
+
end
|
|
34
36
|
end
|
|
35
37
|
RUBY
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def stage_two
|
|
41
|
+
down_msg = "recreate :#{@column} on :#{@table} manually if you need it back"
|
|
39
42
|
write "drop_#{@table}_#{@column}", <<~RUBY
|
|
40
43
|
# frozen_string_literal: true
|
|
41
44
|
|
|
42
|
-
# STEP 2 of 2. Run only after STEP 1 has been deployed and verified
|
|
43
|
-
|
|
45
|
+
# STEP 2 of 2. Run only after STEP 1 has been deployed and verified in
|
|
46
|
+
# production (nothing reads the column, no errors).
|
|
47
|
+
class Drop#{camel}Column < ActiveRecord::Migration[#{migration_version}]
|
|
44
48
|
def up
|
|
45
49
|
remove_column :#{@table}, :#{@column}
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def down
|
|
49
|
-
raise ActiveRecord::IrreversibleMigration,
|
|
50
|
-
"recreate :#{@column} on :#{@table} manually if you need it back"
|
|
53
|
+
raise ActiveRecord::IrreversibleMigration, #{down_msg.inspect}
|
|
51
54
|
end
|
|
52
55
|
end
|
|
53
56
|
RUBY
|
|
@@ -61,6 +64,14 @@ module SchemaReaper
|
|
|
61
64
|
path
|
|
62
65
|
end
|
|
63
66
|
|
|
67
|
+
# Match the host app's Rails minor version when ActiveRecord is loaded,
|
|
68
|
+
# otherwise a sane recent default the developer can edit.
|
|
69
|
+
def migration_version
|
|
70
|
+
return "7.1" unless defined?(ActiveRecord::VERSION::STRING)
|
|
71
|
+
|
|
72
|
+
ActiveRecord::VERSION::STRING.split(".").first(2).join(".")
|
|
73
|
+
end
|
|
74
|
+
|
|
64
75
|
def camel
|
|
65
76
|
"#{@table}_#{@column}".split("_").map(&:capitalize).join
|
|
66
77
|
end
|
data/lib/schema_reaper/runner.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "set"
|
|
|
5
5
|
module SchemaReaper
|
|
6
6
|
# Ties introspection + scanning + runtime data + analyzers together.
|
|
7
7
|
class Runner
|
|
8
|
+
SEVERITY_RANK = { high: 0, medium: 1, low: 2 }.freeze
|
|
9
|
+
|
|
8
10
|
def initialize(config: Config.load, root: Dir.pwd, introspector: nil, runtime: nil)
|
|
9
11
|
@config = config
|
|
10
12
|
@root = root
|
|
@@ -29,13 +31,30 @@ module SchemaReaper
|
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
def dedupe(findings)
|
|
35
|
+
collapse_targets(drop_findings_on_dead_tables(findings))
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
# When a whole table is dead, its per-column and per-index findings are
|
|
33
39
|
# noise -- keep only the table-level finding for that table.
|
|
34
|
-
def
|
|
40
|
+
def drop_findings_on_dead_tables(findings)
|
|
35
41
|
dead_tables = findings.select { |f| f.type == :dead_table }.to_set(&:table)
|
|
36
42
|
findings.reject { |f| f.type != :dead_table && dead_tables.include?(f.table) }
|
|
37
43
|
end
|
|
38
44
|
|
|
45
|
+
# Several analyzers can flag the same physical column or index (e.g.
|
|
46
|
+
# dead_column + always_null_column). Keep the highest-confidence finding per
|
|
47
|
+
# target so it is reported -- and its bytes counted -- exactly once, but note
|
|
48
|
+
# the other analyzers that agreed.
|
|
49
|
+
def collapse_targets(findings)
|
|
50
|
+
findings.group_by(&:target_key).map do |_key, group|
|
|
51
|
+
winner = group.max_by { |f| [f.confidence, -SEVERITY_RANK.fetch(f.severity, 9)] }
|
|
52
|
+
also = group.map(&:type).uniq - [winner.type]
|
|
53
|
+
winner.evidence += ["also flagged by: #{also.join(", ")}"] if also.any?
|
|
54
|
+
winner
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
39
58
|
def schema
|
|
40
59
|
return @introspector.call if @introspector
|
|
41
60
|
|
data/schema_reaper.gemspec
CHANGED
|
@@ -5,8 +5,8 @@ require_relative "lib/schema_reaper/version"
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "schema_reaper"
|
|
7
7
|
spec.version = SchemaReaper::VERSION
|
|
8
|
-
spec.authors = [
|
|
9
|
-
spec.email = ["akshatpegwar5@gmail.com"]
|
|
8
|
+
spec.authors = %w[aksshatt mitkush]
|
|
9
|
+
spec.email = ["akshatpegwar5@gmail.com", "mitanshukushwah@gmail.com"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "Find and safely remove schema dead-weight in Rails + PostgreSQL apps."
|
|
12
12
|
spec.description =
|
metadata
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: schema_reaper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- aksshatt
|
|
8
|
+
- mitkush
|
|
8
9
|
autorequire:
|
|
9
10
|
bindir: exe
|
|
10
11
|
cert_chain: []
|
|
@@ -88,6 +89,7 @@ description: 'schema_reaper finds schema dead-weight in Rails/ActiveRecord + Pos
|
|
|
88
89
|
only for now; Ruby >= 2.7. See the README for full usage.'
|
|
89
90
|
email:
|
|
90
91
|
- akshatpegwar5@gmail.com
|
|
92
|
+
- mitanshukushwah@gmail.com
|
|
91
93
|
executables:
|
|
92
94
|
- schema_reaper
|
|
93
95
|
extensions: []
|