schema_reaper 1.0.0 → 1.0.3

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: 7c7a7ccaeefa1c6ddc5597a3b54ebb358fdac16ba72e8413b79cc70ecdf9223e
4
- data.tar.gz: e6a801a21a746da04c63416669c0cf35e55975b8af85b631502847bdba769189
3
+ metadata.gz: 22245618558de174144cf3732fbced500e89726cafdec2ab14bdefe35d535287
4
+ data.tar.gz: 95a26a72863e7f603d1d6894aed3f89948f34d85959359863db83e9d01cc5d1b
5
5
  SHA512:
6
- metadata.gz: fd33e938eddd3fd6ffb0e50dca359a62ac6974573838605f627a999bed26114d7501f92ae9edb769053ba65b8ee2d5a6f1d5f7b01f84b39f8abb8707ca6113ce
7
- data.tar.gz: aceba9a2f2fdcb55d8b780b5d1e85a7431c9a587f3592e019c3434e7e0e7343d60d10ee8f7063f4b888027f4bdf3c59e7caa09f04b82c1803e3d96afcef239a3
6
+ metadata.gz: f98dcbc99cf5a8527ecdfbce06dfce99f6f678971c9d60ee96a030ba0b71eeac731e39f0e8dc87e697a9d74dcfec95d1fb1cfe06549a5b3db55a36580da42a7c
7
+ data.tar.gz: dbcb412469b0bb09c2676706b5a11a6fb085649411d9af3c29d8e633fd208a9e83315905886cf9bc065af4d85a148cdab11101b9dc6ff09380887effbee17493
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.1
2
+ TargetRubyVersion: 2.7
3
3
  NewCops: enable
4
4
  SuggestExtensions: false
5
5
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.3] - 2026-09-04
4
+
5
+ ### Changed
6
+ - Richer gem metadata for the RubyGems page: expanded `description` (analyzer
7
+ list, safety model, gem-awareness, reporters), and added `bug_tracker_uri`,
8
+ `documentation_uri`, `wiki_uri` and `funding_uri` (GitHub Sponsors) link
9
+ metadata. No code change.
10
+
11
+ ## [1.0.2] - 2026-09-04
12
+
13
+ ### Changed
14
+ - Lowered `required_ruby_version` to `>= 2.7.0` (was `>= 3.1.0`).
15
+ - Rewrote all 40 endless method definitions (`def x = ...`, a Ruby 3.0
16
+ feature) as classic `def ... end`.
17
+ - Added explicit `require "set"` where `Set` / `to_set` are used (autoloaded
18
+ only on Ruby 3.2+).
19
+ - `rubocop` `TargetRubyVersion` set to 2.7; CI matrix now 2.7–3.3.
20
+ - Every `lib/`, `spec/` and `exe/` file verified to parse under Ruby 2.7.0;
21
+ dependency-free modules exercised on a real 2.7 runtime. Runtime deps
22
+ (`prism` >= 2.7, `thor` >= 2.6, `pg`) all support 2.7.
23
+ - No behaviour change; 29 specs unchanged.
24
+
25
+ ## [1.0.1] - 2026-09-04
26
+
27
+ Superseded by 1.0.2 before release. Lowered the floor to `>= 3.0.0` only.
28
+
3
29
  ## [1.0.0] - 2026-09-04
4
30
 
5
31
  First stable release.
data/PRO.md ADDED
@@ -0,0 +1,98 @@
1
+ # schema_reaper Pro — open-core plan
2
+
3
+ The `schema_reaper` gem stays **MIT, free, and complete for its job**: schema
4
+ introspection, the seven analyzers, static + runtime signal, staged migrations,
5
+ `table`/`json`/`markdown`/`sarif` output, the CI baseline gate. Everything a
6
+ solo developer or single app needs.
7
+
8
+ **Pro** is a separate paid layer for *teams and multi-database estates* — the
9
+ work that only pays off at scale and that individuals rarely need.
10
+
11
+ ---
12
+
13
+ ## Free vs Pro
14
+
15
+ | Capability | Free (`schema_reaper`) | Pro (`schema_reaper-pro`) |
16
+ |---|---|---|
17
+ | PostgreSQL adapter | ✅ | ✅ |
18
+ | MySQL / MariaDB adapter | — | ✅ |
19
+ | 7 core analyzers | ✅ | ✅ |
20
+ | Orphan-row + schema-drift analyzers | — | ✅ |
21
+ | Runtime attribute tracker | ✅ (file sink) | ✅ + Redis / StatsD / OTLP sinks |
22
+ | Reporters | table / json / markdown / sarif | + HTML, Slack, Jira, PR review comments |
23
+ | Reclaimable estimate | bytes/row × est. rows | real `pg_total_relation_size`, index bloat, $ per cloud plan |
24
+ | History / trend | local JSONL + `trend` | hosted timeline, burndown charts, per-team dashboards |
25
+ | Baseline gate | one file | per-branch baselines, ownership routing (CODEOWNERS) |
26
+ | Multi-database / shards | one DB per run | fan-out across N databases, divergence report |
27
+ | Scheduled scans + alerts | — | ✅ (cron runner, "queue breached", "new dead column in `billing`") |
28
+ | Mountable dashboard engine | — | ✅ `mount SchemaReaper::Pro::Engine` |
29
+ | SSO, audit log, RBAC | — | ✅ (Enterprise) |
30
+ | Support | GitHub issues, best effort | private issues, SLA |
31
+
32
+ Pro depends on the OSS gem and reuses its analyzer registry — no fork, no
33
+ divergence. New analyzers land in Free first unless they are inherently
34
+ team-scale.
35
+
36
+ ---
37
+
38
+ ## Delivery & licensing
39
+
40
+ - **Distribution**: private gem on a Gemfury / GitHub Packages source, installed
41
+ with a license-key-scoped token.
42
+
43
+ ```ruby
44
+ source "https://pro.schema-reaper.dev" do
45
+ gem "schema_reaper-pro"
46
+ end
47
+ ```
48
+
49
+ - **License check**: offline-friendly signed key (Ed25519), 30-day grace if the
50
+ license server is unreachable. No phone-home of schema content — ever.
51
+ - **License**: commercial EULA, per-organization. Source-available to
52
+ subscribers (read + patch), not redistributable.
53
+ - **Trial**: 21-day full-feature key, no card.
54
+
55
+ ---
56
+
57
+ ## Pricing (draft)
58
+
59
+ | Tier | Who | Price | Limits |
60
+ |---|---|---|---|
61
+ | **Solo Pro** | 1 dev, unlimited personal/side projects | $9 / mo ($90 / yr) | 3 databases |
62
+ | **Team** | up to 10 devs | $49 / mo ($490 / yr) | 15 databases, Slack/Jira, scheduled scans |
63
+ | **Business** | up to 50 devs | $199 / mo | unlimited databases, dashboard engine, priority support |
64
+ | **Enterprise** | 50+ / regulated | custom (from ~$12k / yr) | SSO, audit log, RBAC, on-prem license server, SLA |
65
+
66
+ Free forever for: OSS projects, students, and any single non-commercial
67
+ database. Charity/edu 50% off.
68
+
69
+ ---
70
+
71
+ ## Hosted option (later — biggest ceiling)
72
+
73
+ `schema-reaper.dev` SaaS: point it at a read-replica, get weekly reports, trend
74
+ dashboards, PR comments, and alerts with zero gem install. Same tiers, +$X for
75
+ the managed runner. Ship the self-hosted Pro gem first; the SaaS is Pro + a
76
+ scheduler + a web app.
77
+
78
+ ---
79
+
80
+ ## Rollout order
81
+
82
+ 1. **Now**: Sponsor button, `PRO.md`, "Pro" section in README (waitlist link).
83
+ 2. **v1.1 (OSS)**: MySQL adapter groundwork, real size math — proves demand,
84
+ some lands free.
85
+ 3. **Pro 0.1**: private gem = MySQL + Slack reporter + scheduled scans + license
86
+ key. Sell to the first 10 teams at half price for feedback.
87
+ 4. **Pro 0.2**: dashboard engine, multi-DB fan-out, per-branch baselines.
88
+ 5. **SaaS beta**: once ~25 paying teams self-host.
89
+
90
+ ---
91
+
92
+ ## Principles
93
+
94
+ - Free tier never gets *worse* to sell Pro. No crippling, no nag screens.
95
+ - Anything a single app genuinely needs stays free.
96
+ - Never transmit schema, data, or query text without explicit opt-in.
97
+ - Pro features are the ones a 2-person team can't be bothered to build and a
98
+ 50-person org will happily expense.
data/README.md CHANGED
@@ -124,6 +124,21 @@ require:
124
124
  - Mountable dashboard engine, trend charts
125
125
  - MySQL adapter
126
126
 
127
+ ## Pro (for teams)
128
+
129
+ The gem is free and complete for a single app. **schema_reaper Pro** adds the
130
+ team-scale layer: MySQL adapter, multi-database fan-out, orphan-row and
131
+ schema-drift analyzers, Slack/Jira/PR-comment reporters, real
132
+ `pg_total_relation_size` + $ estimates, scheduled scans with alerts, and a
133
+ mountable dashboard engine. See [PRO.md](PRO.md). Waitlist / early access:
134
+ open an issue tagged `pro`.
135
+
136
+ ## Sponsor
137
+
138
+ `schema_reaper` is MIT and maintained in the open. If it saved you disk, money,
139
+ or a nasty migration, sponsor its development via the **Sponsor** button on the
140
+ repo.
141
+
127
142
  ## Development
128
143
 
129
144
  ```
@@ -9,8 +9,13 @@ module SchemaReaper
9
9
  # gem_columns - Hash{table_name => Set<column_name>} reserved by gems
10
10
  # config - Config
11
11
  Context = Struct.new(:schema, :used_tokens, :runtime, :gem_columns, :config, keyword_init: true) do
12
- def runtime = self[:runtime] || Runtime::Report.empty
13
- def gem_columns = self[:gem_columns] || {}
12
+ def runtime
13
+ self[:runtime] || Runtime::Report.empty
14
+ end
15
+
16
+ def gem_columns
17
+ self[:gem_columns] || {}
18
+ end
14
19
  end
15
20
 
16
21
  # Shared plumbing for analyzers: schema access and token lookup helpers.
@@ -24,16 +29,29 @@ module SchemaReaper
24
29
  end
25
30
 
26
31
  # @return [Array<Finding>]
27
- def call = raise NotImplementedError
32
+ def call
33
+ raise(NotImplementedError)
34
+ end
28
35
 
29
36
  private
30
37
 
31
38
  attr_reader :ctx
32
39
 
33
- def schema = ctx.schema
34
- def config = ctx.config
35
- def runtime = ctx.runtime
36
- def used?(token) = ctx.used_tokens.include?(token.to_s.downcase)
40
+ def schema
41
+ ctx.schema
42
+ end
43
+
44
+ def config
45
+ ctx.config
46
+ end
47
+
48
+ def runtime
49
+ ctx.runtime
50
+ end
51
+
52
+ def used?(token)
53
+ ctx.used_tokens.include?(token.to_s.downcase)
54
+ end
37
55
 
38
56
  def gem_reserved?(table, column)
39
57
  ctx.gem_columns.fetch(table, []).include?(column)
@@ -54,7 +54,9 @@ module SchemaReaper
54
54
  ev
55
55
  end
56
56
 
57
- def camelize(str) = str.split("_").map(&:capitalize).join
57
+ def camelize(str)
58
+ str.split("_").map(&:capitalize).join
59
+ end
58
60
  end
59
61
  end
60
62
  end
@@ -13,7 +13,9 @@ module SchemaReaper
13
13
  @classes << klass unless @classes.include?(klass)
14
14
  end
15
15
 
16
- def all = @classes
16
+ def all
17
+ @classes
18
+ end
17
19
  end
18
20
  end
19
21
  end
@@ -6,7 +6,9 @@ require_relative "../schema_reaper"
6
6
  module SchemaReaper
7
7
  # Command-line entry point. See `exe/schema_reaper`.
8
8
  class CLI < Thor
9
- def self.exit_on_failure? = true
9
+ def self.exit_on_failure?
10
+ true
11
+ end
10
12
 
11
13
  class_option :config, type: :string, default: ".schema_reaper.yml",
12
14
  desc: "path to config file"
@@ -48,13 +50,19 @@ module SchemaReaper
48
50
  end
49
51
 
50
52
  desc "version", "Print version"
51
- def version = say(SchemaReaper::VERSION)
53
+ def version
54
+ say(SchemaReaper::VERSION)
55
+ end
52
56
 
53
57
  private
54
58
 
55
- def config = @config ||= Config.load(options[:config])
59
+ def config
60
+ @config ||= Config.load(options[:config])
61
+ end
56
62
 
57
- def run = Runner.new(config: config).run
63
+ def run
64
+ Runner.new(config: config).run
65
+ end
58
66
 
59
67
  def enforce_baseline(findings)
60
68
  new_ones = Baseline.new(config.baseline_path).new_among(findings)
@@ -44,15 +44,41 @@ module SchemaReaper
44
44
  @data["database_url"] || ENV.fetch("DATABASE_URL", nil)
45
45
  end
46
46
 
47
- def scan_paths = @data["scan_paths"]
48
- def view_globs = @data["view_globs"]
49
- def ignore_tables = @data.dig("ignore", "tables").to_a
50
- def min_age_days = @data["min_age_days"]
51
- def baseline_path = @data["baseline"]
52
- def runtime_log = @data["runtime_log"]
53
- def history_log = @data["history_log"]
54
- def gem_awareness? = @data["gem_awareness"] != false
55
- def require_paths = @data["require"].to_a
47
+ def scan_paths
48
+ @data["scan_paths"]
49
+ end
50
+
51
+ def view_globs
52
+ @data["view_globs"]
53
+ end
54
+
55
+ def ignore_tables
56
+ @data.dig("ignore", "tables").to_a
57
+ end
58
+
59
+ def min_age_days
60
+ @data["min_age_days"]
61
+ end
62
+
63
+ def baseline_path
64
+ @data["baseline"]
65
+ end
66
+
67
+ def runtime_log
68
+ @data["runtime_log"]
69
+ end
70
+
71
+ def history_log
72
+ @data["history_log"]
73
+ end
74
+
75
+ def gem_awareness?
76
+ @data["gem_awareness"] != false
77
+ end
78
+
79
+ def require_paths
80
+ @data["require"].to_a
81
+ end
56
82
 
57
83
  def always_keep_columns
58
84
  @data["always_keep_columns"].to_a
@@ -19,7 +19,9 @@ module SchemaReaper
19
19
  [type, table, column, index].compact.join("/")
20
20
  end
21
21
 
22
- def reclaimable_bytes = self[:reclaimable_bytes] || 0
22
+ def reclaimable_bytes
23
+ self[:reclaimable_bytes] || 0
24
+ end
23
25
 
24
26
  def to_h
25
27
  super.merge(id: id, reclaimable_bytes: reclaimable_bytes)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module SchemaReaper
4
6
  # Columns and tables that popular gems own and reference indirectly (through
5
7
  # metaprogramming the static scanner cannot see). Detected gems get their
@@ -61,7 +61,12 @@ module SchemaReaper
61
61
  path
62
62
  end
63
63
 
64
- def camel = "#{@table}_#{@column}".split("_").map(&:capitalize).join
65
- def model = @table.split("_").map(&:capitalize).join.sub(/s$/, "")
64
+ def camel
65
+ "#{@table}_#{@column}".split("_").map(&:capitalize).join
66
+ end
67
+
68
+ def model
69
+ @table.split("_").map(&:capitalize).join.sub(/s$/, "")
70
+ end
66
71
  end
67
72
  end
@@ -35,7 +35,9 @@ module SchemaReaper
35
35
  end
36
36
  end
37
37
 
38
- def total = @findings.sum(&:reclaimable_bytes)
38
+ def total
39
+ @findings.sum(&:reclaimable_bytes)
40
+ end
39
41
  end
40
42
  end
41
43
  end
@@ -61,7 +61,9 @@ module SchemaReaper
61
61
  end
62
62
  end
63
63
 
64
- def camelize(str) = str.split("_").map(&:capitalize).join
64
+ def camelize(str)
65
+ str.split("_").map(&:capitalize).join
66
+ end
65
67
  end
66
68
  end
67
69
  end
@@ -39,7 +39,9 @@ module SchemaReaper
39
39
  @findings.sort_by { |f| [-f.confidence, SEV_ORDER.fetch(f.severity, 9)] }
40
40
  end
41
41
 
42
- def total_reclaimable = @findings.sum(&:reclaimable_bytes)
42
+ def total_reclaimable
43
+ @findings.sum(&:reclaimable_bytes)
44
+ end
43
45
  end
44
46
  end
45
47
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module SchemaReaper
4
6
  # Ties introspection + scanning + runtime data + analyzers together.
5
7
  class Runner
@@ -11,7 +11,9 @@ module SchemaReaper
11
11
  module Runtime
12
12
  # Aggregated view of a usage log, consumed by analyzers.
13
13
  Report = Struct.new(:accessed, :observed_days, keyword_init: true) do
14
- def self.empty = new(accessed: Set.new, observed_days: 0)
14
+ def self.empty
15
+ new(accessed: Set.new, observed_days: 0)
16
+ end
15
17
 
16
18
  def self.load(path)
17
19
  return empty unless path && File.exist?(path)
@@ -36,9 +38,17 @@ module SchemaReaper
36
38
  ((Time.parse(last) - Time.parse(first)) / 86_400).ceil
37
39
  end
38
40
 
39
- def empty? = accessed.empty?
40
- def present? = !empty?
41
- def read?(table, column) = accessed.include?("#{table}.#{column}")
41
+ def empty?
42
+ accessed.empty?
43
+ end
44
+
45
+ def present?
46
+ !empty?
47
+ end
48
+
49
+ def read?(table, column)
50
+ accessed.include?("#{table}.#{column}")
51
+ end
42
52
  end
43
53
 
44
54
  # Buffered writer for the usage log. Thread-safe append of JSON lines.
@@ -9,26 +9,42 @@ module SchemaReaper
9
9
  keyword_init: true
10
10
  ) do
11
11
  # true when every row holds NULL (pg_stats null_frac == 1)
12
- def always_null? = null_fraction && null_fraction >= 1.0
12
+ def always_null?
13
+ null_fraction && null_fraction >= 1.0
14
+ end
13
15
 
14
16
  # true when the column holds exactly one distinct non-null value
15
- def single_value? = distinct_values == 1
17
+ def single_value?
18
+ distinct_values == 1
19
+ end
16
20
  end
17
21
 
18
22
  Index = Struct.new(:name, :columns, :unique, :primary, :scans, keyword_init: true) do
19
- def covers?(other) = columns.first(other.columns.length) == other.columns
23
+ def covers?(other)
24
+ columns.first(other.columns.length) == other.columns
25
+ end
20
26
  end
21
27
 
22
28
  Table = Struct.new(
23
29
  :name, :columns, :indexes, :primary_key, :foreign_keys, :row_count,
24
30
  keyword_init: true
25
31
  ) do
26
- def column_names = columns.map(&:name)
27
- def column(name) = columns.find { |c| c.name == name }
28
- def index_on(cols) = indexes.find { |i| i.columns == Array(cols) }
32
+ def column_names
33
+ columns.map(&:name)
34
+ end
35
+
36
+ def column(name)
37
+ columns.find { |c| c.name == name }
38
+ end
39
+
40
+ def index_on(cols)
41
+ indexes.find { |i| i.columns == Array(cols) }
42
+ end
29
43
  end
30
44
 
31
45
  DatabaseSchema = Struct.new(:tables, keyword_init: true) do
32
- def table(name) = tables.find { |t| t.name == name }
46
+ def table(name)
47
+ tables.find { |t| t.name == name }
48
+ end
33
49
  end
34
50
  end
@@ -10,7 +10,7 @@ module SchemaReaper
10
10
  # safe, a false "dead" is not.
11
11
  class Scanner
12
12
  RUBY_GLOB = "**/*.rb"
13
- WORD_RE = /[a-z_][a-z0-9_]*/i
13
+ WORD_RE = /[a-z_][a-z0-9_]*/i.freeze
14
14
 
15
15
  # Node classes whose #name (or #unescaped) is a bare identifier we treat
16
16
  # as a possible column/table reference.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchemaReaper
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.3"
5
5
  end
data/lib/schema_reaper.rb CHANGED
@@ -40,5 +40,7 @@ module SchemaReaper
40
40
  "sarif" => Reporters::Sarif
41
41
  }.freeze
42
42
 
43
- def self.reporter(name) = REPORTERS.fetch(name, Reporters::Table)
43
+ def self.reporter(name)
44
+ REPORTERS.fetch(name, Reporters::Table)
45
+ end
44
46
  end
@@ -8,25 +8,51 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["aksshatt"]
9
9
  spec.email = ["akshatpegwar5@gmail.com"]
10
10
 
11
- spec.summary = "Find dead columns, unused indexes and schema dead-weight in Rails/ActiveRecord apps."
11
+ spec.summary = "Find dead columns, dead tables, unused indexes and other schema dead-weight " \
12
+ "in Rails/ActiveRecord apps -- then remove it safely."
12
13
  spec.description = <<~DESC
13
- schema_reaper scans your PostgreSQL schema, its planner statistics and your
14
- codebase to surface columns, indexes and tables that are no longer used:
15
- dead columns and tables, unused and duplicate indexes, missing foreign-key
16
- indexes, always-NULL and single-value columns. Findings are scored by
17
- confidence (fusing an optional runtime attribute-access signal), carry a
18
- reclaimable-bytes estimate, and come with staged, reversible migrations.
19
- Ships table/json/markdown/sarif reporters, a CI baseline gate and a trend
20
- log.
14
+ schema_reaper reads your live PostgreSQL schema and planner statistics and
15
+ cross-references them against a Prism-AST static scan of your codebase
16
+ (models, views, SQL string literals). With an optional runtime signal -- a
17
+ sampled log of which columns are actually read in production -- it fuses
18
+ static and runtime evidence into a confidence score per finding.
19
+
20
+ Analyzers:
21
+ * dead_column -- column no code path references
22
+ * dead_table -- table with no model/query reference, zero rows
23
+ * unused_index -- non-unique index, idx_scan = 0 in pg_stat
24
+ * duplicate_index -- index that is a prefix of a wider index
25
+ * missing_fk_index -- foreign-key / *_id column with no covering index
26
+ * always_null_column -- pg_stats null_frac = 1.0, carries no data
27
+ * single_value_column -- one distinct value across a large table
28
+
29
+ Every finding carries a reclaimable-bytes estimate (from row counts) and a
30
+ concrete fix. `generate-migration` emits a staged, reversible pair:
31
+ ignored_columns first, remove_column after a soak. Nothing is dropped
32
+ automatically. Columns owned by common gems (devise, paper_trail,
33
+ activestorage, actiontext, friendly_id, audited, pg_search, ahoy_matey,
34
+ paranoia) are whitelisted when the gem is in your bundle.
35
+
36
+ Reporters: table, json, markdown (PR comments), SARIF 2.1.0 (GitHub code
37
+ scanning). `scan --ci` gates only findings absent from a committed baseline.
38
+ `trend` keeps an append-only history log for cleanup burndown. A Rails
39
+ railtie adds rake tasks and an opt-in runtime tracker. Custom analyzers
40
+ load through the `require:` config key.
41
+
42
+ PostgreSQL only for now (MySQL adapter on the roadmap). Ruby >= 2.7.
21
43
  DESC
22
44
  spec.homepage = "https://github.com/aksshatt/schema_reaper"
23
45
  spec.license = "MIT"
24
- spec.required_ruby_version = ">= 3.1.0"
46
+ spec.required_ruby_version = ">= 2.7.0"
25
47
 
26
48
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
27
- spec.metadata["homepage_uri"] = spec.homepage
28
- spec.metadata["source_code_uri"] = spec.homepage
29
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
49
+ spec.metadata["homepage_uri"] = spec.homepage
50
+ spec.metadata["source_code_uri"] = spec.homepage
51
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
52
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
53
+ spec.metadata["documentation_uri"] = "#{spec.homepage}#readme"
54
+ spec.metadata["wiki_uri"] = "#{spec.homepage}/wiki"
55
+ spec.metadata["funding_uri"] = "https://github.com/sponsors/aksshatt"
30
56
  spec.metadata["rubygems_mfa_required"] = "true"
31
57
 
32
58
  spec.files = Dir.chdir(__dir__) do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema_reaper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - aksshatt
@@ -79,14 +79,35 @@ dependencies:
79
79
  - !ruby/object:Gem::Version
80
80
  version: '1.5'
81
81
  description: |
82
- schema_reaper scans your PostgreSQL schema, its planner statistics and your
83
- codebase to surface columns, indexes and tables that are no longer used:
84
- dead columns and tables, unused and duplicate indexes, missing foreign-key
85
- indexes, always-NULL and single-value columns. Findings are scored by
86
- confidence (fusing an optional runtime attribute-access signal), carry a
87
- reclaimable-bytes estimate, and come with staged, reversible migrations.
88
- Ships table/json/markdown/sarif reporters, a CI baseline gate and a trend
89
- log.
82
+ schema_reaper reads your live PostgreSQL schema and planner statistics and
83
+ cross-references them against a Prism-AST static scan of your codebase
84
+ (models, views, SQL string literals). With an optional runtime signal -- a
85
+ sampled log of which columns are actually read in production -- it fuses
86
+ static and runtime evidence into a confidence score per finding.
87
+
88
+ Analyzers:
89
+ * dead_column -- column no code path references
90
+ * dead_table -- table with no model/query reference, zero rows
91
+ * unused_index -- non-unique index, idx_scan = 0 in pg_stat
92
+ * duplicate_index -- index that is a prefix of a wider index
93
+ * missing_fk_index -- foreign-key / *_id column with no covering index
94
+ * always_null_column -- pg_stats null_frac = 1.0, carries no data
95
+ * single_value_column -- one distinct value across a large table
96
+
97
+ Every finding carries a reclaimable-bytes estimate (from row counts) and a
98
+ concrete fix. `generate-migration` emits a staged, reversible pair:
99
+ ignored_columns first, remove_column after a soak. Nothing is dropped
100
+ automatically. Columns owned by common gems (devise, paper_trail,
101
+ activestorage, actiontext, friendly_id, audited, pg_search, ahoy_matey,
102
+ paranoia) are whitelisted when the gem is in your bundle.
103
+
104
+ Reporters: table, json, markdown (PR comments), SARIF 2.1.0 (GitHub code
105
+ scanning). `scan --ci` gates only findings absent from a committed baseline.
106
+ `trend` keeps an append-only history log for cleanup burndown. A Rails
107
+ railtie adds rake tasks and an opt-in runtime tracker. Custom analyzers
108
+ load through the `require:` config key.
109
+
110
+ PostgreSQL only for now (MySQL adapter on the roadmap). Ruby >= 2.7.
90
111
  email:
91
112
  - akshatpegwar5@gmail.com
92
113
  executables:
@@ -100,6 +121,7 @@ files:
100
121
  - CHANGELOG.md
101
122
  - CODE_OF_CONDUCT.md
102
123
  - LICENSE.txt
124
+ - PRO.md
103
125
  - README.md
104
126
  - Rakefile
105
127
  - exe/schema_reaper
@@ -143,6 +165,10 @@ metadata:
143
165
  homepage_uri: https://github.com/aksshatt/schema_reaper
144
166
  source_code_uri: https://github.com/aksshatt/schema_reaper
145
167
  changelog_uri: https://github.com/aksshatt/schema_reaper/blob/main/CHANGELOG.md
168
+ bug_tracker_uri: https://github.com/aksshatt/schema_reaper/issues
169
+ documentation_uri: https://github.com/aksshatt/schema_reaper#readme
170
+ wiki_uri: https://github.com/aksshatt/schema_reaper/wiki
171
+ funding_uri: https://github.com/sponsors/aksshatt
146
172
  rubygems_mfa_required: 'true'
147
173
  post_install_message:
148
174
  rdoc_options: []
@@ -152,7 +178,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
152
178
  requirements:
153
179
  - - ">="
154
180
  - !ruby/object:Gem::Version
155
- version: 3.1.0
181
+ version: 2.7.0
156
182
  required_rubygems_version: !ruby/object:Gem::Requirement
157
183
  requirements:
158
184
  - - ">="
@@ -162,6 +188,6 @@ requirements: []
162
188
  rubygems_version: 3.4.10
163
189
  signing_key:
164
190
  specification_version: 4
165
- summary: Find dead columns, unused indexes and schema dead-weight in Rails/ActiveRecord
166
- apps.
191
+ summary: Find dead columns, dead tables, unused indexes and other schema dead-weight
192
+ in Rails/ActiveRecord apps -- then remove it safely.
167
193
  test_files: []