Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
7 changes: 1 addition & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`.

Expand Down
5 changes: 5 additions & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ 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`. |
| [`primary_key_type`](primary_key_type.md) | warning | Primary keys typed as `integer` / `smallint` will eventually overflow. |
| [`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

Expand Down
5 changes: 5 additions & 0 deletions docs/severity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down