Skip to content

Add Voice Arena database schema - #119

Merged
seribaymadina merged 4 commits into
mainfrom
feat/voice-arena
Jun 17, 2026
Merged

Add Voice Arena database schema#119
seribaymadina merged 4 commits into
mainfrom
feat/voice-arena

Conversation

@seribaymadina

@seribaymadina seribaymadina commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

First piece of Voice Arena — just the database tables, nothing wired up yet.

Adds an arena schema (next to benchmarks_v2, same DB) with three tables:

  • battles — a matchup: prompt, the two models, their audio. Model names are text keys matching benchmarks_v2.results, so they can be joined later. A model can't battle itself.
  • votes — one judgment per row (A_WIN/B_WIN/TIE), labeler vs external. UNIQUE (battle_id, voter_type, voter_id) is the double-vote backstop; updated_at lets a labeler re-label (upsert) without breaking windowed refits, while external votes stay insert-once.
  • leaderboard_snapshots — computed Elo / Bradley-Terry ratings over time. UNIQUE (computed_at, metric_name, methodology_version, domain, provider, model) enforces one row per model per board; domain is NOT NULL DEFAULT 'all' so the global board dedups too.

Additive only: no grants, no benchmarks_v2 changes, nothing reads/writes these yet. The arena DB user + grants live in Terraform, landing separately.

Greptile Summary

Introduces the arena schema alongside benchmarks_v2, adding three tables (battles, votes, leaderboard_snapshots) for the Voice Arena feature. The migration is additive-only and well-documented; prior review rounds addressed the missing UNIQUE constraint on snapshots, the absent updated_at trigger, and type-uniformity for win/loss/tie columns.

  • arena.battles stores raw matchups with a CHECK that prevents a model from facing itself; arena.votes records human judgments with a BEFORE UPDATE trigger that auto-maintains updated_at; arena.leaderboard_snapshots caches computed Elo/BT ratings with a 6-column UNIQUE constraint.
  • The leaderboard_snapshots board-grouping relies on computed_at TIMESTAMPTZ DEFAULT now() as a shared key across rows, but now() returns the transaction start time — multi-transaction snapshot writes will silently assign different timestamps to rows in the same conceptual run, fragmenting the board and defeating the UNIQUE deduplication guarantee.

Confidence Score: 3/5

The migration is additive and won't touch existing tables, but the board-grouping design in leaderboard_snapshots has a structural flaw that should be resolved before any snapshot writer is built against it.

The snapshot table's deduplication guarantee — enforced by a UNIQUE constraint that includes computed_at — breaks as soon as a writer inserts rows across multiple transactions. Each transaction gets a different timestamp from DEFAULT now(), making rows that belong to the same computation run appear as separate boards. Fixing the schema now (before any writer is implemented) is straightforward; waiting until writers exist makes it a much more disruptive change.

runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py — specifically the leaderboard_snapshots table definition and its UNIQUE constraint.

Important Files Changed

Filename Overview
runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py Adds arena schema with battles, votes, and leaderboard_snapshots tables. Previous-round fixes are in place (UNIQUE on snapshots, updated_at trigger, uniform NUMERIC types). One remaining structural issue: the board-grouping key for leaderboard_snapshots relies on DEFAULT now() timestamps which produce different values across transactions, potentially fragmenting a single computation run into multiple boards and undermining the UNIQUE deduplication guarantee.

Entity Relationship Diagram

%%{init: {'theme': 'neutral'}}%%
erDiagram
    BATTLES {
        UUID id PK
        TEXT provider_a
        TEXT model_a
        TEXT provider_b
        TEXT model_b
        TEXT domain
        TEXT prompt_text
        TEXT audio_a_url
        TEXT audio_b_url
        TIMESTAMPTZ created_at
    }

    VOTES {
        UUID id PK
        UUID battle_id FK
        TEXT outcome
        TEXT voter_type
        TEXT voter_id
        TIMESTAMPTZ created_at
        TIMESTAMPTZ updated_at
    }

    LEADERBOARD_SNAPSHOTS {
        UUID id PK
        TIMESTAMPTZ computed_at
        TEXT metric_name
        TEXT methodology_version
        TEXT domain
        TEXT provider
        TEXT model
        NUMERIC rating_elo
        NUMERIC rating_bt
        NUMERIC ci_low
        NUMERIC ci_high
        NUMERIC ci_half_width
        INTEGER votes_total
        NUMERIC wins
        NUMERIC losses
        NUMERIC ties
        TEXT status
    }

    BATTLES ||--o{ VOTES : "has"
    BATTLES }o--o{ LEADERBOARD_SNAPSHOTS : "informs (via refit)"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
erDiagram
    BATTLES {
        UUID id PK
        TEXT provider_a
        TEXT model_a
        TEXT provider_b
        TEXT model_b
        TEXT domain
        TEXT prompt_text
        TEXT audio_a_url
        TEXT audio_b_url
        TIMESTAMPTZ created_at
    }

    VOTES {
        UUID id PK
        UUID battle_id FK
        TEXT outcome
        TEXT voter_type
        TEXT voter_id
        TIMESTAMPTZ created_at
        TIMESTAMPTZ updated_at
    }

    LEADERBOARD_SNAPSHOTS {
        UUID id PK
        TIMESTAMPTZ computed_at
        TEXT metric_name
        TEXT methodology_version
        TEXT domain
        TEXT provider
        TEXT model
        NUMERIC rating_elo
        NUMERIC rating_bt
        NUMERIC ci_low
        NUMERIC ci_high
        NUMERIC ci_half_width
        INTEGER votes_total
        NUMERIC wins
        NUMERIC losses
        NUMERIC ties
        TEXT status
    }

    BATTLES ||--o{ VOTES : "has"
    BATTLES }o--o{ LEADERBOARD_SNAPSHOTS : "informs (via refit)"
Loading

Reviews (4): Last reviewed commit: "Auto-bump arena.votes.updated_at via tri..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
benchmarks Ready Ready Preview, Comment Jun 17, 2026 9:13pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new Alembic migration (20260615_0007) is added that creates the arena schema and three tables: battles, votes, and leaderboard_snapshots. It sets UUID primary keys, a foreign key from votes.battle_id to battles.id, CHECK constraints on outcome/type columns, and indexes on battles.domain, votes.battle_id, and a composite (metric_name, computed_at) on snapshots. The downgrade() drops the schema with CASCADE.

Changes

Arena Schema Migration

Layer / File(s) Summary
Migration metadata and module docs
runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py
Module docstring documents the arena schema and its three managed tables; revision and down_revision constants chain this migration after 20260611_0006.
Schema DDL: tables, constraints, indexes, and rollback
runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py
upgrade() creates the arena schema, then the battles, votes, and leaderboard_snapshots tables with UUID PKs (gen_random_uuid()), CHECK constraints on outcome/type fields, a FK from votes.battle_idbattles.id, and three indexes. downgrade() drops the schema with CASCADE.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop hop, a schema is born today,
Three tables lined up in arena array!
Battles and votes with UUIDs bright,
Snapshots of leaderboards gleaming at night.
CASCADE drops them clean away —
The rabbit migration has earned its hay! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Voice Arena database schema' clearly and concisely summarizes the main change—introducing the Voice Arena database schema with three new tables in a separate arena schema.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/voice-arena

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py Outdated
Comment thread runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py Outdated
Comment thread runner/src/coval_bench/db/migrations/versions/20260615_0007_arena_init_schema.py Outdated
Schema-qualify arena objects instead of relying on search_path, make the
snapshot count columns uniformly NUMERIC, widen the snapshot lookup index to
the full board grouping, and add votes.updated_at to support labeler re-label
windowing.
…hecks

Prevent a model battling itself, enforce one snapshot row per board (domain
NOT NULL with an 'all' sentinel so global rows dedup too), drop the redundant
lookup index, and add nonnegative checks on snapshot counts.
Add a BEFORE UPDATE trigger so a re-label always stamps updated_at, even if
the writer omits the column — windowed refits can't silently use a stale time.
@seribaymadina
seribaymadina requested a review from coval-cale June 17, 2026 21:18

@coval-cale coval-cale left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@seribaymadina
seribaymadina merged commit c1478ea into main Jun 17, 2026
6 checks passed
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.

2 participants