Database Schema
Version 1 · Initial DB schema reference, verified against config/settings.py and queries.py
Runner Form — Database Schema
Connects to MySQL database qdb at 138.226.220.168:3306 (user web_admin), configured in
config/settings.py. There is no Django ORM — every table below is queried with raw SQL from
runner_form/queries.py.
Naming inconsistency:
CLAUDE.mdandPLAN.mdin this repo refer to the database asqdg, butconfig/settings.pyliterally setsDATABASES['default']['NAME'] = 'qdb'. Code is the ground truth — this document usesqdb. See [[bugs]].
Core tables
runner — horse identity
Queried as runner h in get_horse(). Columns actually selected: id, runner_name,
country_code, year_of_foal, sex, colour, discipline, age, sire_name, sire_id,
sire_year, dam_name, dam_id, dam_year, dam_sire_name, trainer_id,
display_foal_date, foal_date, owner, master_rating.
The full runner table (per qdg-form-lookup-field-binding-spec.md in the repo root) also
carries horse_type, breeding_id, dam_rating, gelding_date, current_trainer,
location, silk_description, hemisphere, bred_by, stable, tf_season, tf_career,
comment, tf_master_rating, tf_symbol, runner_pm — not currently selected by this app.
run — individual race starts
Joined as run r in _base_run_select(), the one wide query nearly every page builds on. Key
columns used: id, runner_id, runner_name, race_id, discipline, finish_position,
tab_number, margin, did_not_finish, is_disqualified, runner_time(_sec), weight,
age_at, rating, pos_settling, vp_1200/1000/800/600/400/200 (sectional position, aliased
pos_*), pos_turn, pos_1200/pos_800 (raw), official_inrunning(_short),
timeform_rating/timeform_symbol, official_rating, claim_alt, weight_alt,
beaten_margin, stewards_long/short, timeform_comment, gear_change, is_blinkers_on,
jockey_id/name, trainer_id/name, barrier_position, starting_price_value,
starting_price, opening_odds, jockey_claim, runner_sectional, prize_won_orig,
prize_aud.
race
Joined as race rc. Columns used: id, race_date, race_number, race_name(_short),
distance(_val/_imp), group_status, class_level, race_age_restriction,
race_sex_restriction, race_discipline, race_type, sot(_rating), surface,
prize_money(_val), time_winner(_sec), number_runners, sectional_time1, sectional_dist,
winners_30days, course_id, rmeeting_id.
race_discipline:T=Thoroughbred,H=Harness,G=Greyhound. The form guide and API endpoints filter toT(and historicallyGper the API docx) only.surface:T=Turf,D=Dirt,AW=All-Weather,S=Synthetic.sot(state of track):G=Good,S=Soft,H=Heavy,F=Firm.
meeting
Columns used: id, race_date, course_id, country_id, rails, penetrometer, mpc
(M/P/C = Metropolitan/Provincial/Country), is_official_results, discipline.
courses, jockey, trainers, country
courses:id,display_name,name,state,direction.jockey:id,firstname,surname.trainers:id,trainer_name.country:id,iso_alpha,name.
Supporting tables
| Table | Purpose |
|---|---|
trial_race, trial_run, trial_meeting |
Barrier trial history (get_trial_runs). trial_race.class_level, sot, time_winner_sec are NULL for all rows currently. |
sales, sales_lots, sales_lookup |
Yearling sale records (get_sales_info) — joined by runner name when there's no direct runner_id link. |
generic_lookup |
Free-text lookup joined four ways in _base_run_select (jockey summary, post-race comment, pre-race comment, stewards advice). |
Known data gaps (confirmed by investigation)
run.vp_1200/800/600/400/200 sectional-position columns are NULL for all races before
~2021 — confirmed by a full-table investigation: 43,952,078 total rows in run; for the
2015–2020 window (11,473,723 rows, covering all of WINX's career) every vp_* column returns
exactly 0. Rows from 2020 onward are populated (~639,000+ rows with data per column). Affects
any horse whose entire career predates the ~2021 data-load cutoff. Not a code defect — the
application correctly renders — for NULL values. See [[bugs]] for the full note.
Planned schema direction (not yet implemented)
qdg-form-lookup-field-binding-spec.md (repo root) documents a proposed redesign: keep the
internal object named runner (already true in code, contrary to the older README which still
refers to a horse table), stop hard-coding example data in templates, and have the backend
return one flattened object ({runner, summary, quick_stats, runs}) instead of the frontend
joining raw tables. It also lists additional runner/run/race columns and tables
(stewards, sectional_run, sectional_race) not currently queried by this app. Treat it as a
design reference for a future iteration, not the current schema.
See [[architecture]] for how this data layer is used, and [[api-reference]] for what each endpoint returns from it.