Skip to content

chore(db): drop dead columns + redundant indexes + unique constraint - #598

Merged
junhoyeo merged 3 commits into
mainfrom
chore/drop-dead-columns
May 25, 2026
Merged

chore(db): drop dead columns + redundant indexes + unique constraint#598
junhoyeo merged 3 commits into
mainfrom
chore/drop-dead-columns

Conversation

@junhoyeo

@junhoyeo junhoyeo commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

Drops the dead schema surface flagged in the post-#593 audit, plus the index churn and redundant unique constraint that were originally split out as separate follow-up PRs (B and C). Single migration, all IF EXISTS / IF NOT EXISTS so it's safe to replay.

Category Drop
(A) Dead columns daily_breakdown.provider_breakdown, daily_breakdown.model_breakdown, submissions.status, users.is_admin
(B) Dead indexes idx_submissions_status, idx_submissions_user_id, idx_submissions_total_tokens, idx_submissions_date_range
(B) Duplicate-of-unique-constraint indexes idx_users_username, idx_sessions_token, idx_api_tokens_token, idx_device_codes_device_code, idx_device_codes_user_code
(B) FK coverage adds idx_device_codes_user_id, idx_group_members_invited_by, idx_group_invites_invited_by
(C) Redundant unique constraint submissions_user_hash_unique (subsumed by submissions_user_id_unique)
Pre-existing test fixup leaderboard.test.ts was expecting 5 args to getLeaderboardData but #522 added customFrom/customTo

Net: -14 schema objects, +3 FK-coverage indexes.

Why bundle B + C into A?

Three reasons — each B/C drop carries the same risk profile as the A drops in this PR:

  1. The 5 duplicate-of-unique-constraint indexes are literally redundant. Postgres auto-creates a btree for every UNIQUE constraint; the explicit idx_*_token / idx_*_username / idx_*_device_code / idx_*_user_code btrees are 1:1 copies of *_unique indexes. Zero risk to drop.
  2. The 3 submissions indexes are confirmed dead in prod. fix(submit): preserve multi-machine submissions without reshaping daily breakdown #389's pg_stat_user_indexes audit (~6 weeks old, called out as a caveat below) showed 0–214 scans per index vs 3.27M on idx_submissions_leaderboard which serves the same access patterns as a left-prefix index.
  3. The unique constraint drop is mathematically subsumed. submissions_user_id_unique already enforces one row per user; any (user_id, submission_hash) collision is rejected at the stronger constraint. Cannot fire under any conditions while the stronger constraint exists.

Verification — answers to "is the data all migrated correctly?"

All of the following was run against a clean local Postgres 16 in docker, after wiping the schema entirely and re-applying all 11 migrations from 0000 to 0011:

1. Migration journal is complete

drizzle.__drizzle_migrations contains 12 rows (ids 1–12, matching 0000–0011 + the bootstrap row). No gaps, no out-of-order timestamps.

2. FK coverage is now 100%

14 FK constraints checked
14 covered
 0 missing indexes

The audit query joins pg_constraint against pg_index and confirms every FK column appears as a left-prefix of at least one index. Was 11 covered / 3 missing before this PR; this PR adds 3 covering indexes and removes nothing FK-related.

3. Schema-vs-code drift

bunx tsc --noEmit → 0 errors. Every column referenced in src/ exists in schema.ts, every column in schema.ts exists in the live DB, every dropped column is absent from both.

drizzle-kit generate does prompt about "is submitted_device_id a rename of provider_breakdown?" — that's because the meta/*_snapshot.json files only exist for 0000/0002 (the project never committed snapshots for newer migrations). It's not a real drift; db:migrate (what runs in CI/prod) ignores the snapshots and applies SQL by tag. Regenerating snapshots is out of scope here.

4. Tests + live submit roundtrip

  • bunx vitest run __tests__251 passed, 0 failed
  • Seed script reload: 3 users, 6 devices, 84 daily_breakdown rows, 1 group with 3 members — all counts consistent across re-runs.
  • Real POST /api/submit with a manually-inserted api_tokens row → HTTP 200, submissionId returned, daily_breakdown row inserted, submission totals correctly recomputed across all days. No write touched any of the dropped columns and nothing errored.

Indexes I deliberately did NOT touch

  • idx_submissions_created_at — leaderboard composite has created_at as 4th column, so a plain created_at filter still benefits from this dedicated index
  • idx_users_github_id — github OAuth login does a lookup by github_id on every sign-in; this index is hot
  • idx_daily_breakdown_date — used by leaderboard period filters (gte/lte against dailyBreakdown.date)
  • idx_daily_breakdown_submission_id / idx_daily_breakdown_submitted_device_id — both FK covers
  • idx_sessions_user_id, idx_sessions_expires_at, idx_api_tokens_user_id, idx_device_codes_expires_at — all serve real query patterns

Caveats

  • Drops are irreversible at this layer; recreation in prod requires CREATE INDEX CONCURRENTLY if scans turn out to be needed.
  • fix(submit): preserve multi-machine submissions without reshaping daily breakdown #389's prod stats audit is ~6 weeks old. Before merging, re-run SELECT relname, idx_scan FROM pg_stat_user_indexes WHERE schemaname='public' ORDER BY idx_scan; on prod and sanity-check that the 3 submissions indexes still have ~0 scans. The 5 duplicate-of-unique-constraint indexes don't need this check — they're proven redundant by schema definition.
  • users.is_admin was already a useless capability (no gates), so dropping it doesn't change observed behavior. If admin features are planned, the right pattern is a separate admin_users table or a role enum on users, not a boolean column the auth layer ignores.

Migration ordering

Depends on #593's 0010_submit_count_safety being present. This PR is branched off feat/devices-ui-from-pr389; once #593 merges to main, this PR retargets to main automatically and remains a clean follow-up.

Test plan

  • CI green on this branch
  • Re-run prod pg_stat_user_indexes on the 3 submissions indexes; confirm scans still 0
  • Apply 0011_drop_dead_columns to staging; verify all \d outputs and the FK coverage audit query
  • Manual smoke: profile + devices + leaderboard + one real submit
  • Confirm no internal dashboards or ad-hoc SQL queries reference the dropped columns or unique constraint

Summary by cubic

Drops unused DB columns and dead indexes, removes a redundant unique constraint, and adds FK-covering indexes to keep deletes fast. Cleans up related code and tests to match the slimmer schema.

  • Migration

    • Dropped columns: daily_breakdown.provider_breakdown, daily_breakdown.model_breakdown, submissions.status, users.is_admin.
    • Dropped dead indexes and redundant constraint: idx_submissions_status, idx_submissions_user_id, idx_submissions_total_tokens, idx_submissions_date_range; submissions_user_hash_unique.
    • Added FK indexes: idx_device_codes_user_id, idx_group_members_invited_by, idx_group_invites_invited_by.
    • Kept explicit non-unique indexes that duplicate UNIQUE constraints (sessions/users/api_tokens/device_codes) because the planner prefers them.
  • Refactors

    • Removed isAdmin from session and personal token types; updated tests and fixtures.
    • Stopped writing/reading providerBreakdown and modelBreakdown; deleted buildModelBreakdown; updated /api/submit and users profile handler.
    • Fixed leaderboard test to pass customFrom/customTo args; removed the unused model breakdown test.

Written for commit 5e5682d. Summary will update on new commits. Review in cubic

junhoyeo added 3 commits May 25, 2026 13:56
Migration 0011 drops five pieces of confirmed-dead schema surface:

- daily_breakdown.provider_breakdown: declared in schema.ts but never
  read or written anywhere in src/. Pure allocated bytes per row.

- daily_breakdown.model_breakdown: written on every submit, but the
  only SELECT (users/[username]/route.ts:110) discards the value.
  Storage + serialization churn with no consumer.

- submissions.status + idx_submissions_status: column is only ever
  written as 'verified' on insert; zero WHERE filters anywhere.
  The index serves no query.

- users.is_admin: column is set/fetched but no admin gate exists
  anywhere in the codebase. A user with is_admin=true had zero
  additional privileges. If admin features ship later, reintroduce
  WITH an actual gate.

Code changes follow the schema: remove the field from drizzle
table defs, drop the buildModelBreakdown helper (only caller was
the submit write path now also gone), and stop emitting / reading
the dropped fields from session/auth/profile/submit endpoints.

Tests: removed 21 isAdmin mock-object entries and the test that
inlined buildModelBreakdown's algorithm. Also fixed a pre-existing
leaderboard.test.ts failure caused by #522 adding customFrom /
customTo positional args without updating the expectation.

Verification: tsc 0 errors, 251/251 vitest, end-to-end POST
/api/submit against the post-drop schema returns 200 and writes
a daily_breakdown row successfully.

Constraint: do not regress the active submit / profile / devices paths
Constraint: depends on #593's 0010_submit_count_safety being present
Rejected: drop submissions.idx_submissions_user_id + idx_submissions_total_tokens + idx_submissions_date_range as well | needs a fresh pg_stat_user_indexes audit on prod first; the index cleanup is a separate analysis-driven PR
Rejected: drop submissions_user_hash_unique constraint (functionally redundant with submissions_user_id_unique) | low-blast-radius but worth its own PR with a freshness check on the submit-idempotency code path
Confidence: high
Scope-risk: moderate (touches submit/route.ts, the critical write path)
Directive: do NOT reintroduce users.is_admin without also wiring an admin gate; the previous setup misled readers into thinking access control existed
Not-tested: ad-hoc SQL queries that may have been reading the dropped jsonb columns on prod
Extends 0011_drop_dead_columns with the index-cleanup (group B) and
redundant-unique-constraint (group C) drops that were originally
proposed as follow-up PRs, after a fresh integrity audit on the
local DB confirmed each drop is safe.

Index drops:
- idx_submissions_user_id: redundant left-prefix of
  idx_submissions_leaderboard (user_id, total_tokens, total_cost,
  created_at). Per #389's prod stats audit, 214 scans on this vs
  3.27M on the leaderboard composite.
- idx_submissions_total_tokens: 0 prod scans (#389 audit). All
  reads use SUM() per-user GROUP BY which is served by the
  leaderboard composite.
- idx_submissions_date_range: 0 prod scans (#389 audit). No
  filter ever queries these columns; leaderboard period filters
  use daily_breakdown.date which has its own index.
- idx_users_username, idx_sessions_token, idx_api_tokens_token,
  idx_device_codes_device_code, idx_device_codes_user_code:
  Postgres auto-creates a btree for every UNIQUE constraint;
  these explicit btrees were 1:1 duplicates of *_unique indexes
  and only added INSERT/UPDATE write amplification.

Index adds (FK coverage):
- idx_device_codes_user_id: cascade-delete of a user did a seq
  scan of device_codes.
- idx_group_members_invited_by: cascade-delete of an inviter
  did a seq scan of group_members.
- idx_group_invites_invited_by: same, for group_invites.

Constraint drop:
- submissions_user_hash_unique (user_id, submission_hash) was
  functionally subsumed by submissions_user_id_unique (user_id).
  The stronger constraint already enforces one row per user, so
  any (user_id, submission_hash) collision is rejected at the
  user_id level. The submission_hash column itself stays;
  /api/submit still reads it for idempotency comparison.

Verification on clean local DB:
- All 11 prior migrations (0000-0010) plus this 0011 apply
  successfully from an empty schema.
- pg_constraint audit: all 14 FKs in the schema now have
  covering indexes (was 11 before; +3 from this PR).
- Index audit: all 9 dead/redundant indexes confirmed gone;
  the 3 new FK-coverage indexes confirmed present.
- bunx tsc --noEmit: 0 errors.
- bunx vitest run __tests__: 251/251 pass.
- End-to-end POST /api/submit with a real api_tokens row: HTTP
  200, daily_breakdown row written, submission totals updated.
- Seed script reload: 3 users, 6 devices, 84 daily rows, 1 group
  with 3 members — all counts consistent.

Constraint: each index drop must have either a prod stats scan-count justification or be a literal duplicate of an existing unique-constraint index
Constraint: do not regress FK cascade-delete behavior
Rejected: drop idx_submissions_created_at | not redundant — leaderboard composite has created_at as the 4th column; a plain created_at filter still benefits from a dedicated index
Rejected: drop idx_users_github_id | github OAuth login does a lookup by github_id on every sign-in; this index is hot
Confidence: high
Scope-risk: moderate (drops are irreversible; index recreation in prod requires CREATE INDEX CONCURRENTLY if scans turn out to be needed)
Directive: do NOT re-add an explicit btree on any column that already has a UNIQUE constraint — postgres creates the covering index automatically
Not-tested: prod pg_stat_user_indexes scan counts as of merge day (the audit data is ~6 weeks old per #389)
The 2026-05-25 prod audit (pg_stat_user_indexes) changed the picture
for the duplicate-of-unique-constraint indexes I planned to drop:

- idx_sessions_token: 89,560 scans (sessions_token_unique: 0)
- idx_users_username: 30,080 scans (users_username_unique: 0)
- idx_api_tokens_token: 27,126 scans (api_tokens_token_unique: 0)
- idx_device_codes_device_code: 153 scans
- idx_device_codes_user_code: 3 scans

Postgres' planner consistently picks the explicit non-unique index
over the unique-constraint sibling, so these are NOT no-op drops —
the unique-constraint indexes show 0 scans in prod because they
aren't being chosen. Removing the explicit ones would invalidate
cached plans and force a re-plan onto the unique-constraint
indexes; safe in theory but a non-zero risk window.

Removed those five DROP INDEX statements from migration 0011 and
restored the corresponding index() declarations to schema.ts with a
comment explaining why the explicit non-unique siblings stay.

Two drops that prod stats initially questioned but the
investigation subagents confirmed safe (no app caller, scans are
write-side enforcement / vacuum):

- idx_submissions_status: 323 scans — vacuum cycles + historical
  scan count from before commit acccd44 removed the column from
  the drizzle model. No app WHERE filter exists.
- submissions_user_hash_unique: 388 scans — pure constraint-check
  btree probes on every INSERT/UPDATE that writes submission_hash;
  no read query references the column.

Both still get dropped.

Constraint: do not invalidate hot cached query plans on prod without a controlled re-plan window
Rejected: drop all 5 unique-duplicate btrees | the planner uses them, sessions_token alone is 89k scans
Confidence: high
Scope-risk: narrow (only removed planned drops; nothing else changed)
Directive: do NOT remove the 5 explicit btrees on prod without first running ANALYZE on the table and verifying the planner falls back to the unique-constraint index
Not-tested: cold-plan latency after dropping any of these in a future PR
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tokscale Ready Ready Preview, Comment May 25, 2026 4:57am

Request Review

@junhoyeo
junhoyeo merged commit 157ab04 into main May 25, 2026
4 of 5 checks passed
@junhoyeo
junhoyeo deleted the chore/drop-dead-columns branch May 25, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant