schema_reaper 2.0.0 → 2.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 +4 -4
- data/.rubocop.yml +3 -0
- data/.schema_reaper.yml.example +17 -0
- data/CHANGELOG.md +144 -0
- data/PRO.md +11 -6
- data/README.md +421 -71
- data/RELEASE_CHECKLIST.md +13 -2
- data/lib/generators/schema_reaper/install/install_generator.rb +11 -11
- data/lib/generators/schema_reaper/install/templates/initializer.rb.tt +8 -0
- data/lib/schema_reaper/alert_config.rb +21 -1
- data/lib/schema_reaper/alert_state.rb +35 -0
- data/lib/schema_reaper/analyzers/always_null_column.rb +12 -5
- data/lib/schema_reaper/analyzers/base.rb +66 -2
- data/lib/schema_reaper/analyzers/dead_column.rb +12 -6
- data/lib/schema_reaper/analyzers/dead_table.rb +10 -6
- data/lib/schema_reaper/analyzers/duplicate_foreign_key.rb +53 -0
- data/lib/schema_reaper/analyzers/duplicate_index.rb +1 -1
- data/lib/schema_reaper/analyzers/missing_fk_index.rb +10 -4
- data/lib/schema_reaper/analyzers/sequence_exhaustion.rb +55 -0
- data/lib/schema_reaper/analyzers/single_value_column.rb +17 -5
- data/lib/schema_reaper/analyzers/unused_index.rb +11 -6
- data/lib/schema_reaper/baseline.rb +95 -10
- data/lib/schema_reaper/cli/baseline_support.rb +45 -0
- data/lib/schema_reaper/cli.rb +69 -14
- data/lib/schema_reaper/config.rb +28 -0
- data/lib/schema_reaper/doctor.rb +151 -0
- data/lib/schema_reaper/explain.rb +102 -0
- data/lib/schema_reaper/gem_awareness.rb +31 -1
- data/lib/schema_reaper/inflect.rb +36 -0
- data/lib/schema_reaper/introspect/postgres.rb +258 -55
- data/lib/schema_reaper/migration_generator.rb +90 -4
- data/lib/schema_reaper/notifier.rb +69 -8
- data/lib/schema_reaper/railtie.rb +1 -1
- data/lib/schema_reaper/reporters/sarif.rb +15 -6
- data/lib/schema_reaper/reporters/slack.rb +67 -0
- data/lib/schema_reaper/runner.rb +58 -6
- data/lib/schema_reaper/runtime.rb +94 -11
- data/lib/schema_reaper/scan_job.rb +24 -2
- data/lib/schema_reaper/schema.rb +32 -3
- data/lib/schema_reaper/schema_locator.rb +75 -0
- data/lib/schema_reaper/static/cache.rb +66 -0
- data/lib/schema_reaper/static/scanner.rb +174 -6
- data/lib/schema_reaper/tasks/schema_reaper.rake +2 -2
- data/lib/schema_reaper/version.rb +1 -1
- data/lib/schema_reaper.rb +18 -1
- data/schema_reaper.gemspec +1 -2
- metadata +13 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65c5a4cc1857d1fbf940b5907cbb7d8e48de74cebb7b69eb6dd1b8e2a0c26bfa
|
|
4
|
+
data.tar.gz: 679e01d41a797c37dfd94858464127b1ce8789bda1d92cb905f011a2369fe1c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef8476dfea36578ef5c97c43fc97c7334371ce38aa14f0d736028d57e42d370b43363922511676af417d76f5fd9b9b35536e98168253efc9c6a663230c56d8c6
|
|
7
|
+
data.tar.gz: cf4f22deed425352ac3d0553deefb34495e4c234170f402167866fa445b4b4bf707fc3d078a3a612fc99477ed39b9c152f96f4cdfb45555ecfa7a0eaaab9cd82
|
data/.rubocop.yml
CHANGED
|
@@ -31,6 +31,7 @@ Metrics/BlockLength:
|
|
|
31
31
|
- "*.gemspec"
|
|
32
32
|
|
|
33
33
|
Metrics/ParameterLists:
|
|
34
|
+
CountKeywordArgs: false # keyword args name themselves at every call site
|
|
34
35
|
Exclude:
|
|
35
36
|
- "spec/**/*"
|
|
36
37
|
|
|
@@ -45,6 +46,8 @@ Metrics/MethodLength:
|
|
|
45
46
|
|
|
46
47
|
Metrics/ClassLength:
|
|
47
48
|
Max: 135
|
|
49
|
+
Exclude:
|
|
50
|
+
- "lib/schema_reaper/introspect/postgres.rb" # mostly SQL heredocs, one per catalog query
|
|
48
51
|
|
|
49
52
|
Metrics/CyclomaticComplexity:
|
|
50
53
|
Max: 12
|
data/.schema_reaper.yml.example
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
---
|
|
3
3
|
# database_url: postgres://localhost/myapp_development # else ENV["DATABASE_URL"]
|
|
4
4
|
|
|
5
|
+
# PostgreSQL schemas to scan. Tables outside public are reported as schema.table.
|
|
6
|
+
schemas:
|
|
7
|
+
- public
|
|
8
|
+
|
|
9
|
+
# Hide findings below this confidence on every command (the CLI flag overrides).
|
|
10
|
+
min_confidence: 0.0
|
|
11
|
+
|
|
5
12
|
scan_paths:
|
|
6
13
|
- app
|
|
7
14
|
- lib
|
|
@@ -33,11 +40,21 @@ gem_awareness: true
|
|
|
33
40
|
|
|
34
41
|
# Runtime usage log written by the tracker (see README > Runtime signal).
|
|
35
42
|
runtime_log: .schema_reaper/runtime.jsonl
|
|
43
|
+
# Rotate it to runtime.jsonl.1 past this many bytes; both are read. null: never.
|
|
44
|
+
runtime_log_max_bytes: 52428800
|
|
36
45
|
|
|
37
46
|
# Append-only snapshot log for `schema_reaper trend`.
|
|
38
47
|
history_log: .schema_reaper/history.jsonl
|
|
39
48
|
|
|
40
49
|
baseline: .schema_reaper/baseline.json
|
|
41
50
|
|
|
51
|
+
# Per-file cache of scan results; repeat scans re-parse only changed files.
|
|
52
|
+
scan_cache: .schema_reaper/scan_cache.json
|
|
53
|
+
|
|
54
|
+
# global: a column name anywhere in code counts as use on every table.
|
|
55
|
+
# model: a name only in an app/models file counts for that model's table and
|
|
56
|
+
# the tables it mentions. Stricter; review its findings before acting.
|
|
57
|
+
column_scope: global
|
|
58
|
+
|
|
42
59
|
# Extra Ruby files to load before analysis -- register custom analyzers here.
|
|
43
60
|
require: []
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.1.0] - 2026-10-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Introspection no longer mixes up same-named objects across schemas.**
|
|
7
|
+
Row counts were read from `pg_class` by name alone, foreign keys were joined
|
|
8
|
+
on constraint name alone (constraint names are only unique per table), and
|
|
9
|
+
the primary-key lookup resolved the table through `search_path`. All three
|
|
10
|
+
are now qualified by schema. Foreign keys are read from `pg_constraint`, so
|
|
11
|
+
composite keys keep their columns together.
|
|
12
|
+
- **`ignore.columns` now applies to every column analyzer.** It was only
|
|
13
|
+
honoured by `dead_column`; `always_null_column`, `single_value_column` and
|
|
14
|
+
`missing_fk_index` ignored it.
|
|
15
|
+
- **Partitioned tables are reported once, as the parent.** Each partition used
|
|
16
|
+
to appear as its own table -- and, unreferenced by name, got a `dead_table`
|
|
17
|
+
finding. The parent's row count is now summed from its partitions, and its
|
|
18
|
+
indexes' usage from the partitions' indexes, so `unused_index` no longer
|
|
19
|
+
flags every index on a partitioned table (PostgreSQL 12+).
|
|
20
|
+
- **Stats-based findings say when they are estimates.** On tables larger than
|
|
21
|
+
ANALYZE's 30,000-row sample, `always_null_column` (0.85 -> 0.7) and
|
|
22
|
+
`single_value_column` (0.6 -> 0.5) lower their confidence and say so, and
|
|
23
|
+
`single_value_column` no longer claims "the same value in every row" for a
|
|
24
|
+
column that also holds NULLs.
|
|
25
|
+
- **`missing_fk_index`** finds the referenced table in a non-public table's own
|
|
26
|
+
schema.
|
|
27
|
+
- **Runtime `observed_days` no longer depends on line order.** Several
|
|
28
|
+
processes append to one log, so the span is now min..max timestamp rather
|
|
29
|
+
than first..last line.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- `schemas:` config key to scan PostgreSQL schemas besides `public`. Tables
|
|
33
|
+
outside `public` are reported as `schema.table`; `public` tables keep their
|
|
34
|
+
bare names, so existing baselines and ignore lists still match.
|
|
35
|
+
- `sequence_exhaustion` analyzer (CYCLE and descending sequences excluded): a serial/IDENTITY sequence past 50% (medium)
|
|
36
|
+
or 75% (high) of its column type's range -- e.g. an `integer` key fed by a
|
|
37
|
+
bigint sequence.
|
|
38
|
+
- `duplicate_foreign_key` analyzer: two FK constraints on the same columns
|
|
39
|
+
referencing the same key, with the same ON UPDATE / ON DELETE, MATCH and
|
|
40
|
+
deferrability (anything else changes behaviour when dropped). A validated
|
|
41
|
+
copy is kept over a NOT VALID one.
|
|
42
|
+
- `--only` / `--except` to choose analyzers (custom ones included), and a
|
|
43
|
+
`min_confidence` config key used by every command.
|
|
44
|
+
- `generate-migration TABLE --index NAME` (concurrent `remove_index` with
|
|
45
|
+
`disable_ddl_transaction!`; the migration checks at run time whether the
|
|
46
|
+
index is partitioned, which PostgreSQL can't drop concurrently, and drops it
|
|
47
|
+
plainly then) and `generate-migration TABLE` (`drop_table`). The
|
|
48
|
+
`unused_index` / `duplicate_index` fixes say the same for partitioned
|
|
49
|
+
indexes.
|
|
50
|
+
- The runtime tracker also samples tables read by executed SQL (`track_sql:`,
|
|
51
|
+
on by default; FROM / JOIN / UPDATE, not INSERT targets, so a write-only
|
|
52
|
+
table can still be found), so a table read only through raw SQL isn't
|
|
53
|
+
reported dead.
|
|
54
|
+
- The runtime log rotates to `<path>.1` past `runtime_log_max_bytes` (default
|
|
55
|
+
50 MB), under a file lock so concurrent workers can't rotate twice; both
|
|
56
|
+
generations are read.
|
|
57
|
+
- `schema_reaper doctor`: checks the connection, statistics age, tables never
|
|
58
|
+
`ANALYZE`d, query history, replica status, runtime log length, expired
|
|
59
|
+
baseline entries, the schema file and alert config.
|
|
60
|
+
- `schema_reaper explain ID` (full or partial id): evidence, confidence,
|
|
61
|
+
reclaim, schema line, baseline status and the `generate-migration` command.
|
|
62
|
+
- `# schema_reaper:keep TARGET -- reason` comments in code silence findings.
|
|
63
|
+
- `column_scope: model` (opt-in): a column name seen only in a model file
|
|
64
|
+
counts for that model's table and the tables it mentions, not every table.
|
|
65
|
+
Models are identified by their full class path (`Admin::User` is not
|
|
66
|
+
`User`); STI subclasses map to their parent's table (resolved the way Ruby
|
|
67
|
+
resolves the constant), namespaced models to both possible defaults, and
|
|
68
|
+
abstract bases, POROs and concerns stay shared.
|
|
69
|
+
- Baseline entries carry an optional reason and expiry (`baseline --reason
|
|
70
|
+
--expires YYYY-MM-DD`); `scan --ci` fails on expired ones again, and passing
|
|
71
|
+
`--reason` / `--expires` renews them. `baseline --prune` drops resolved
|
|
72
|
+
entries without accepting new findings. With `--only` / `--except`,
|
|
73
|
+
`baseline` replaces or prunes only those analyzers' entries. The 2.0
|
|
74
|
+
`{"ids": [...]}` file is still read, and the file keeps an `ids` key
|
|
75
|
+
(unexpired entries) so a 2.0.x gem sharing it still works -- though 2.0.x
|
|
76
|
+
ignores reasons and expiry, and rewrites the file in the old format if it
|
|
77
|
+
runs `baseline`.
|
|
78
|
+
- Scheduled reports say what changed since the last one (via `Rails.cache`);
|
|
79
|
+
`config.alert_on = :changes` sends only then. A report only counts as sent
|
|
80
|
+
once a channel accepted it -- a non-2xx webhook response is a failure -- so
|
|
81
|
+
a failed delivery is retried next run instead of suppressed.
|
|
82
|
+
- Slack webhooks get a Block Kit summary instead of a Markdown table Slack
|
|
83
|
+
can't render; other receivers keep the full `{"text": ...}` report
|
|
84
|
+
(`config.webhook_format`).
|
|
85
|
+
- Gem awareness covers solid_queue, solid_cache, solid_cable, good_job,
|
|
86
|
+
delayed_job_active_record, flipper-active_record, acts-as-taggable-on,
|
|
87
|
+
doorkeeper, noticed, activerecord-session_store, discard, acts_as_list and
|
|
88
|
+
ancestry. Every analyzer now leaves a gem-owned table's columns alone.
|
|
89
|
+
- Per-file scan cache (`scan_cache:`), so repeat scans only re-parse changed
|
|
90
|
+
files. Keyed by a SHA-256 digest, so it survives across processes.
|
|
91
|
+
- SARIF results point at the defining line in `db/schema.rb` or
|
|
92
|
+
`db/structure.sql` instead of the top of `db/schema.rb`.
|
|
93
|
+
- `--color` / `--no-color` works on every command.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
- Column byte estimates use `pg_stats.avg_width` scaled by the non-NULL share
|
|
97
|
+
when the table has been analysed, instead of a fixed per-type guess (16
|
|
98
|
+
bytes for text/jsonb) that also counted NULLs as full width. An all-NULL
|
|
99
|
+
column is estimated at ~0 bytes.
|
|
100
|
+
- `unused_index` evidence notes that `idx_scan` is per server, so indexes used
|
|
101
|
+
only on read replicas look unused on the primary.
|
|
102
|
+
- CI tests Ruby 3.4 and 4.0 too, and runs the live introspection specs against
|
|
103
|
+
a PostgreSQL 16 service.
|
|
104
|
+
|
|
105
|
+
## [2.0.1] - 2026-09-23
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- **Report emails now use the app's `ApplicationMailer`.** `SchemaReaper::Mailer`
|
|
109
|
+
was defined while the gem loaded (`Bundler.require`), before the app's
|
|
110
|
+
autoloader could see `app/mailers/application_mailer.rb`, so it always
|
|
111
|
+
inherited `ActionMailer::Base` and sent from the `schema_reaper@localhost`
|
|
112
|
+
placeholder -- which most SMTP relays reject. It is now autoloaded on first
|
|
113
|
+
use, after boot. The install generator's `from:` check now also accepts
|
|
114
|
+
`config.action_mailer.default_options` for apps without an
|
|
115
|
+
`ApplicationMailer`. 2.0.0 workaround: add
|
|
116
|
+
`SchemaReaper::Mailer.default from: "..."` to the initializer.
|
|
117
|
+
- **`rake schema_reaper:alert` no longer silently drops the scan on the
|
|
118
|
+
`:async` ActiveJob adapter** (the default before Rails 8). The job ran on a
|
|
119
|
+
thread inside the rake process, which exited first -- so the whenever and
|
|
120
|
+
plain-cron schedules never scanned. The task now runs the scan inline (with
|
|
121
|
+
email delivered immediately) when the adapter is `:async`, and enqueues as
|
|
122
|
+
before otherwise.
|
|
123
|
+
- **A missing `pg` gem now gives a clear error.** `pg` is deliberately not a
|
|
124
|
+
runtime dependency (the gem uses the host app's own), but without it the
|
|
125
|
+
scan failed with `NameError: uninitialized constant ...::PG` from the
|
|
126
|
+
connection rescue. It now says to add `gem "pg"`.
|
|
127
|
+
- **`min_age_days` in `.schema_reaper.yml` now takes effect.** It was defined
|
|
128
|
+
and documented since 1.0.0 but never read -- `dead_column` used its own
|
|
129
|
+
hardcoded 14-day threshold for trusting runtime data. The analyzer now reads
|
|
130
|
+
the config key (default still 14, so nothing changes unless you set it).
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
- README rewritten around a quickstart, with animated SVG illustrations
|
|
134
|
+
(`docs/assets/`, excluded from the packaged gem) and corrected details on
|
|
135
|
+
scheduling, the manual trigger's cooldown, CI and gem awareness. Diagrams
|
|
136
|
+
for each major section; animations loop with a long hold on the finished
|
|
137
|
+
frame, since GitHub can't start a README image's animation on scroll.
|
|
138
|
+
Documents the `pg` requirement, that only the `public` schema is scanned, and
|
|
139
|
+
that lists in `.schema_reaper.yml` replace their defaults, and what
|
|
140
|
+
`min_age_days` controls.
|
|
141
|
+
- The `automation`, `safety` and `ci` section diagrams carry label text meant
|
|
142
|
+
to be read, so they no longer loop a fade in/out -- the reveal plays once
|
|
143
|
+
and holds the finished frame, instead of pulling attention away from the
|
|
144
|
+
text every 12s. The safety timeline's progress line keeps a continuous
|
|
145
|
+
moving-dash accent once grown in, since that's motion, not text.
|
|
146
|
+
|
|
3
147
|
## [2.0.0] - 2026-09-23
|
|
4
148
|
|
|
5
149
|
### Added
|
data/PRO.md
CHANGED
|
@@ -24,7 +24,7 @@ work that only pays off at scale and that individuals rarely need.
|
|
|
24
24
|
| History / trend | local JSONL + `trend` | hosted timeline, burndown charts, per-team dashboards |
|
|
25
25
|
| Baseline gate | one file | per-branch baselines, ownership routing (CODEOWNERS) |
|
|
26
26
|
| Multi-database / shards | one DB per run | fan-out across N databases, divergence report |
|
|
27
|
-
| Scheduled scans + alerts |
|
|
27
|
+
| Scheduled scans + alerts | ✅ self-hosted (`rails g schema_reaper:install`: whenever/sidekiq-cron + webhook/email) | ✅ hosted, zero-install + native Slack/Jira routing + per-team alert rules |
|
|
28
28
|
| Mountable dashboard engine | — | ✅ `mount SchemaReaper::Pro::Engine` |
|
|
29
29
|
| SSO, audit log, RBAC | — | ✅ (Enterprise) |
|
|
30
30
|
| Support | GitHub issues, best effort | private issues, SLA |
|
|
@@ -59,7 +59,7 @@ team-scale.
|
|
|
59
59
|
| Tier | Who | Price | Limits |
|
|
60
60
|
|---|---|---|---|
|
|
61
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 |
|
|
62
|
+
| **Team** | up to 10 devs | $49 / mo ($490 / yr) | 15 databases, Slack/Jira, hosted scheduled scans |
|
|
63
63
|
| **Business** | up to 50 devs | $199 / mo | unlimited databases, dashboard engine, priority support |
|
|
64
64
|
| **Enterprise** | 50+ / regulated | custom (from ~$12k / yr) | SSO, audit log, RBAC, on-prem license server, SLA |
|
|
65
65
|
|
|
@@ -82,10 +82,15 @@ scheduler + a web app.
|
|
|
82
82
|
1. **Now**: Sponsor button, `PRO.md`, "Pro" section in README (waitlist link).
|
|
83
83
|
2. **v1.1 (OSS)**: MySQL adapter groundwork, real size math — proves demand,
|
|
84
84
|
some lands free.
|
|
85
|
-
3. **
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
3. **v2.0 (OSS, shipped)**: self-hosted scheduled scans + webhook/email alerts
|
|
86
|
+
(`rails g schema_reaper:install`) — this landed free rather than Pro; it's
|
|
87
|
+
the kind of single-app automation the "anything a single app genuinely
|
|
88
|
+
needs stays free" principle below is about.
|
|
89
|
+
4. **Pro 0.1**: private gem = MySQL + Slack/Jira reporters + hosted scan
|
|
90
|
+
runner + license key. Sell to the first 10 teams at half price for
|
|
91
|
+
feedback.
|
|
92
|
+
5. **Pro 0.2**: dashboard engine, multi-DB fan-out, per-branch baselines.
|
|
93
|
+
6. **SaaS beta**: once ~25 paying teams self-host.
|
|
89
94
|
|
|
90
95
|
---
|
|
91
96
|
|