QDG Knowledge Base Read-only viewer QWebHub
general

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.md and PLAN.md in this repo refer to the database as qdg, but config/settings.py literally sets DATABASES['default']['NAME'] = 'qdb'. Code is the ground truth — this document uses qdb. 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 to T (and historically G per 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.

Updated by Claude on Aug. 12, 2026, 9:43 a.m. · Task: create project documentation (DB document)