Rename tgfp_nfl id columns to provider-neutral external ids + add data_source #368
Labels
No labels
Kestra
bug
enhancement
someday
subtask
☁️ api
🎛️ infrastructure
🐞 sentry
📆 2025 Season
📝 pages
allpicks
📝 pages
picks
📝 pages
standings
🚀 performance
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
johnsturgeon/tgfp-web#368
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2 of the TGFPNfl → ESPNNfl vestige cleanup. Phase 1 (dead code, internal
renames, docs) landed separately; this is the schema half, deliberately split
because it carries a migration.
Prerequisite for the ESPN → TheSportsDB data-source swap.
Context
Two columns still carry the name of a library that no longer exists:
game.tgfp_nfl_game_idstrix_game_tgfp_nfl_game_id— UNIQUE btreeteam.tgfp_nfl_team_idstrix_team_tgfp_nfl_team_id— UNIQUE btreeBoth are described in the models as
"External TGFP/NFL game id", but theyhold ESPN ids and will hold TheSportsDB ids after the swap.
Decisions already settled — do not reopen
All internal relationships go through integer PKs (
Game.home_team_id → team.id,PlayerGamePick.game_id/player_id/picked_team_id). Renaming issafe for referential integrity.
game(each row carries both teams, both scores, and the season);
Team.wins/ losses/tiesis a display cache for the current record only.update_gameonly ever touches in-flight games and never revisits history.A clean cutover is therefore sufficient — no backfill of historical
provider data is required.
data_sourcewill be added even though there is no immediate consumer.It costs one small column now and preserves a programmatic route to
re-fetch provider data later; without it, post-swap the column holds two id
namespaces with no way to tell them apart.
Multi-season correctness of player records is a separate, unrelated issue.
Scope
1. Rename the columns
game.tgfp_nfl_game_id→game.external_game_idteam.tgfp_nfl_team_id→team.external_team_idUpdate the
description=text on bothField(...)definitions, whichcurrently says "External TGFP/NFL …".
2. Add
data_sourceTo both
gameandteam. Follow the existing enum idiom inapp/models/award.py:7:Backfill every existing row to
espn. Add with aserver_default, then dropthe default in the same migration — precedent:
alembic/versions/1bba3f7edaac_remove_server_defaults_from_new_columns.py.3. Move uniqueness to a composite
Single-column uniqueness becomes wrong the moment two providers coexist: the
same id string could legitimately appear once per provider.
Replace the unique index with a composite
UniqueConstraint, leading withdata_sourceso equality lookups on both columns are served:game:(data_source, external_game_id)team:(data_source, external_team_id)In-repo precedent for
__table_args__with a namedUniqueConstraint:app/models/player_award.py:16–25.⚠️ Migration hazards
Alembic autogenerate does not detect column renames. It will emit a
drop_column+add_columnpair, which silently destroys every external idin both tables. The generated migration must be hand-edited to:
Review the generated file before applying it, and confirm the
downgrade()path reverses the rename rather than dropping columns.
The unique indexes must also be renamed or dropped/recreated —
ix_game_tgfp_nfl_game_idandix_team_tgfp_nfl_team_id— since both namesembed the old column name. Verify with
\d gameand\d teamafter applyingthat the composite constraint exists and no orphaned single-column unique
index remains (which would reject legitimate cross-provider duplicates).
Do not edit existing migrations.
9ff5e4af6bbc_01_initial_schema.pyand1885e311300d_02_added_field_uniqueness.pyreference the old names as appliedhistory. A repo-wide find-and-replace would corrupt the revision chain. This
change is a new revision:
Code sites to update (6)
Phase 1 already removed a seventh, in
app/espn_nfl/espn_nfl.py. The datasource module no longer references the schema at all — keep it that way.
app/models/game.pyapp/models/team.pyapp/jobs/create_picks.pyTeamrowapp/jobs/create_picks.pyGamecreationapp/jobs/sync_team_records.pyapp/jobs/update_game.pyThe three lookups in
create_picks.pyuse.one(), so they raiseNoResultFoundon a miss. They should also filter ondata_sourceonce itexists, otherwise a stale ESPN row could satisfy a TheSportsDB lookup.
Open decision: how
teamholds its mappinggameis unambiguous — each row is historical and immutable, so 2025 rowskeep
data_source='espn'forever while new rows get'thesportsdb'. Thecolumn approach is exactly right there.
teamis different: one row per franchise, so adata_sourcecolumn onteammeans one mapping at a time.data_sourcecolumn onteam(simpler). At cutover,overwrite all 32 rows with TheSportsDB ids. The ESPN mapping is lost, but
it is re-derivable (32 stable franchises, matchable by name). Keeps both
tables symmetrical.
team_external_idtable (team_id,data_source,external_id, unique on(data_source, external_id)). Both providerscoexist, so TheSportsDB ids can be populated and verified while ESPN is
still live, making cutover a flag flip and rollback trivial.
Recommendation: Option 1, on the grounds already established — team data
is a display cache, the franchise set is small and stable, and a re-map script
is cheap. Choose Option 2 only if a dual-run verification period before
cutover is wanted.
Note this decision only affects
team;gameuses the column either way.Acceptance criteria
alter_column, withno data loss — row counts and id values identical before/after
data_sourcepresent ongameandteam, all existing rowsespn,no lingering server default
gone
downgrade()verified to reverse cleanlycreate_picks.pylookups filter ondata_sourcepylintreports 10.00/10create_picks,sync_team_recordsandupdate_gamestillresolve teams and games against the live provider
Verification
Before and after the migration:
Then confirm no vestiges remain outside applied migration history:
Expected result: only
alembic/versions/9ff5e4af6bbc_01_initial_schema.pyand
alembic/versions/1885e311300d_02_added_field_uniqueness.py.Repo has no test suite, so verification is lint plus a manual boot and an
exercise of the three affected jobs.