Fix AgeGroup enum Python↔Postgres mismatch (names lowercase, values uppercase) #446
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api#446
Loading…
Add table
Add a link
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?
Type
Bug
Lineage
Standalone — latent bug discovered 2026-04-10/11 during basketball-api#425 end-to-end testing. When Jacelyn's tier-change mint tried to move her to the newly-created 16U Local Queens team (team id 12), the endpoint 500'd with
LookupError: 'U16' is not among the defined enum values. Enum name: agegroup.Repo
forgejo_admin/basketball-apiWhat Broke
The
AgeGroupPython enum insrc/basketball_api/models.py:170defines:Enum NAMES are lowercase (
u16) but enum VALUES are uppercase (U16). The Postgresagegroupenum type only accepts{U8, U10, U12, U14, U16, U18}(uppercase — the values).SQLAlchemy is configured to read enums by NAME. So when SQLAlchemy reads a row with
age_group = 'U16'(the actual stored value matching Postgres enum), it tries to find a Python enum member NAMEDU16— which doesn't exist. Onlyu16(lowercase) exists. RaisesLookupError.Why it didn't surface before 2026-04-10: All 7 pre-existing teams have
age_group = NULL. None of the pre-existing migrations ever populated the column. Migration 040 (040_create_16u_local_queens_team.py) was the first migration to insert a non-NULL value, and it inserted'U16'. Next time a read hit team 12, the LookupError fired.Current workaround: Ava ran
UPDATE teams SET age_group = NULL WHERE id = 12directly on prod to match the other 7 teams. The mismatch is still in the model, just not being exercised.Repro Steps
9598c4dor later), readsrc/basketball_api/models.py:170and confirmAgeGroup.u16 = "U16"from basketball_api.models import AgeGroup; AgeGroup('U16')— succeeds (by VALUE lookup)AgeGroup['U16']— FAILS with KeyError (by NAME lookup)Expected Behavior
The Python enum and the Postgres enum should agree on casing. Two possible fixes:
Option A — lowercase everywhere: change the Python enum values to lowercase (
u16 = "u16") and ALTER the Postgres enum type to lowercase. Requires a migration that renames the enum values. Clean, but touches existing data if any teams ever had uppercase values stored (none currently, so safe).Option B — uppercase everywhere: change the Python enum names to uppercase (
U16 = "U16"). Simpler migration, no enum rename. But breaks Python convention (enum names are usually lowercase or PascalCase, not UPPERCASE).Option C — use
values_callable: passvalues_callable=lambda x: [e.value for e in x]to the SQLAlchemyEnumcolumn type so SQLAlchemy stores/reads by VALUE instead of NAME. No migration needed, no model change needed, just a one-line Column def tweak. This is the correct answer in most cases and preserves the Python convention.Recommended: Option C.
Environment
teams(age_group column)Acceptance Criteria
Team.age_groupcolumn type usesvalues_callableto store/read by value (Option C recommended)UPDATE teams SET age_group = NULL WHERE id = 12reversed — team 12 can holdAgeGroup.u16without blowing up readsdb.query(Team).filter(Team.id == 12).first()returns without LookupErrorage_group=AgeGroup.u16, commit, re-query, assert.age_group == AgeGroup.u16Test Expectations
test_team_age_group_roundtrip— create, read, assert equalitytest_team_age_group_null_still_works— ensure NULL reads don't errorage_group='U16'for team 12Constraints
Related
westside-basketball— project this affects