diff --git a/README.md b/README.md index bd7e2c1..593e892 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Exit codes: `0` = clean, `1` = issues found, `2` = error. | [`fk_type_mismatch`](docs/rules/fk_type_mismatch.md) | FK columns whose type differs from the referenced PK/unique column. | | [`fk_without_on_delete`](docs/rules/fk_without_on_delete.md) | FKs with no explicit `ON DELETE` (defaults to `NO ACTION`, rarely the intended choice). | | [`redundant_index`](docs/rules/redundant_index.md) | Indexes whose column list is a strict prefix of another index on the table. | +| [`unused_index`](docs/rules/unused_index.md) | Secondary indexes that have never been scanned (`pg_stat_user_indexes.idx_scan = 0`). | | [`not_valid_constraints`](docs/rules/not_valid_constraints.md) | FK and CHECK constraints stuck at `NOT VALID` (`convalidated = false`). | | [`sequence_drift`](docs/rules/sequence_drift.md) | Sequences whose `nextval` would collide with rows already in the table. | | [`three_state_boolean`](docs/rules/three_state_boolean.md) | Boolean columns without `NOT NULL` (true / false / null is rarely intended). | diff --git a/ROADMAP.md b/ROADMAP.md index 4e9cb02..c62914b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -76,14 +76,9 @@ When a checker hits `statement_timeout` or is version-gated, the skip goes to st ## Medium-term — more rules -Get from 9 to ~30 rules so the tool is taken seriously next to schemacrawler. High-leverage adds, in rough order: +Get to ~30 rules so the tool is taken seriously next to schemacrawler. High-leverage adds, in rough order: -- **`unused_index`** — `pg_stat_user_indexes.idx_scan = 0` over a configurable window. Pairs naturally with `redundant_index`. -- **`varchar_length`** — `varchar(N)` where `text` would be equivalent (Postgres has no perf benefit to a length cap). -- **`timestamp_without_tz`** — `timestamp` columns that should almost certainly be `timestamptz`. -- **`json_over_jsonb`** — `json` columns that should be `jsonb`. - **`missing_replica_identity`** — tables with no `REPLICA IDENTITY` (extends `missing_primary_key` to cover tables with explicit replica identity but no PK). -- **`fk_without_on_delete`** — foreign keys without an explicit `ON DELETE` policy (forcing the team to think about cascade vs restrict vs set null). - **`partition_without_pk`** — partitioned tables where the partition key isn't part of the PK. - **`default_now_text`** — `default 'now()'` (string literal!) instead of `default now()`. diff --git a/docs/rules/README.md b/docs/rules/README.md index 557e147..8b9cda9 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -6,6 +6,8 @@ Each rule has a dedicated page covering what it catches, why it matters, how to | --- | --- | --- | | [`column_value_at_risk`](column_value_at_risk.md) | warning | Sequence-backed columns whose sequence is past 70% of its type's max. | | [`fk_type_mismatch`](fk_type_mismatch.md) | error | FK column type differs from the referenced column type. | +| [`fk_without_on_delete`](fk_without_on_delete.md) | warning | FKs with no explicit `ON DELETE` (defaults to `NO ACTION`, rarely intended). | +| [`json_over_jsonb`](json_over_jsonb.md) | info | Columns typed `json`; `jsonb` supports indexing and is faster on read. | | [`missing_fk_index`](missing_fk_index.md) | warning | Foreign-key columns not covered by a leading index — slow cascades and joins. | | [`missing_primary_key`](missing_primary_key.md) | warning | Ordinary tables without a primary key. | | [`not_valid_constraints`](not_valid_constraints.md) | error | FK and CHECK constraints stuck at `NOT VALID`. | @@ -13,6 +15,9 @@ Each rule has a dedicated page covering what it catches, why it matters, how to | [`redundant_index`](redundant_index.md) | info | An index whose column list is a strict prefix of another on the same table. | | [`sequence_drift`](sequence_drift.md) | error | Sequence's next value would collide with rows already in the column. | | [`three_state_boolean`](three_state_boolean.md) | warning | Nullable boolean columns turn `bool` into three-valued logic. | +| [`timestamp_without_tz`](timestamp_without_tz.md) | warning | Columns typed `timestamp` (without time zone) — silent timezone bugs. | +| [`unused_index`](unused_index.md) | info | Secondary indexes never scanned per `pg_stat_user_indexes.idx_scan`. | +| [`varchar_length`](varchar_length.md) | info | `varchar(N)` columns where `text` is equivalent in Postgres. | ## Severity philosophy diff --git a/docs/severity.md b/docs/severity.md index b4f7e2e..96b48b0 100644 --- a/docs/severity.md +++ b/docs/severity.md @@ -38,7 +38,9 @@ By default, `pgsleuth check` runs at `--min-severity info`, which means warnings | `primary_key_type` | `integer` PK overflow is months or years away on most tables; the migration to fix it is the painful part. | | `column_value_at_risk` | Sequence-backed columns past 70% of `max_value` will overflow soon. Reactive companion to `primary_key_type` — fires before the failure but after the runway is short. | | `missing_fk_index` | Slow cascades and slow joins. Often invisible until a parent-side `DELETE` migration or a 10× traffic spike. | +| `fk_without_on_delete` | FKs default to `NO ACTION`, which silently blocks parent deletes. Forcing the team to pick `CASCADE` / `RESTRICT` / `SET NULL` makes the policy explicit. | | `three_state_boolean` | `WHERE col = false` silently excludes nulls. Bug surfaces as "where did half my users go?" under SQL three-valued logic. | +| `timestamp_without_tz` | `timestamp` columns store wall-clock time with no zone — comparisons across regions silently disagree, daylight-saving crossings duplicate or skip rows. | ## info — schema smells worth knowing @@ -47,6 +49,9 @@ A rule fires at `info` when the cost is real but small or context-dependent enou | Rule | Why it's info | | --- | --- | | `redundant_index` | Write amplification, disk space, plan-flip risk are real, but rarely production-critical. False positives are conceivable (the prefix index occasionally pulls weight via `INCLUDE` columns or planner quirks). | +| `unused_index` | Pure write-cost on a never-scanned index, but stat-window false positives are easy to trip into (recently-created index, recent stats reset, replica-only reads, infrequent batch jobs). | +| `json_over_jsonb` | `jsonb` is the right choice for almost every workload (indexable, faster on read), but `json` is sometimes deliberate for write-once payloads or to preserve key order. | +| `varchar_length` | `varchar(N)` and `text` are equivalent in Postgres; the cap is decorative. Worth flagging, but rarely worth a migration on its own. | ## Choosing severity for a new rule