5.8 KiB
Stadium Identity System
How SportsTime handles stadium renames, new stadiums, and team relocations while preserving user data.
Architecture Overview
The system uses immutable canonical IDs for all references and mutable display data for names/locations. User data only stores canonical IDs, so it never needs migration when real-world changes occur.
User data → canonical IDs (stable) → current display names (via StadiumIdentityService)
Data Models
Canonical Data (Synced from CloudKit)
// Core identity - canonicalId NEVER changes
CanonicalStadium:
canonicalId: "stadium_nba_los_angeles_lakers" // Immutable
name: "Crypto.com Arena" // Can change
city: "Los Angeles" // Can change
deprecatedAt: nil // Set when demolished
CanonicalTeam:
canonicalId: "team_mlb_athletics" // Immutable
stadiumCanonicalId: "stadium_mlb_las_vegas" // Can change (relocation)
city: "Las Vegas" // Can change
deprecatedAt: nil // Set if team ceases
// Historical name tracking
StadiumAlias:
aliasName: "staples center" // Lowercase for matching
stadiumCanonicalId: "stadium_nba_los_angeles_lakers"
validFrom: 2021-01-01
validUntil: 2022-12-25
TeamAlias:
aliasValue: "Oakland"
aliasType: .city
teamCanonicalId: "team_mlb_athletics"
validUntil: 2024-12-31
User Data (Local Only)
StadiumVisit:
stadiumId: String // Canonical ID - stable reference
stadiumNameAtVisit: String // Frozen at visit time for history
homeTeamId: String? // Canonical ID
awayTeamId: String? // Canonical ID
homeTeamName: String? // Frozen at visit time
awayTeamName: String? // Frozen at visit time
Achievement:
achievementTypeId: String // League structure ID (e.g., "mlb_al_west")
sport: String?
visitIdsSnapshot: Data // UUIDs of StadiumVisits - immutable
Scenario Handling
Stadium Renames
Example: Staples Center → Crypto.com Arena (December 2021)
What happens:
- CloudKit updates
CanonicalStadium.nameto "Crypto.com Arena" - CloudKit adds
StadiumAliasfor "Staples Center" with validity dates - Next sync updates local SwiftData
canonicalIdremains"stadium_nba_los_angeles_lakers"
User impact: None
- Existing
StadiumVisit.stadiumIdstill resolves correctly StadiumVisit.stadiumNameAtVisitpreserves "Staples Center" for historical display- Searching "Staples Center" still finds the stadium via alias lookup
New Stadium Built
Example: New Las Vegas A's stadium opens in 2028
What happens:
- CloudKit adds new
CanonicalStadiumrecord with new canonical ID - Next sync creates record in SwiftData
- Deterministic UUID generated:
SHA256(canonicalId) → UUID - Stadium appears in app automatically
User impact: None
- New stadium available for visits and achievements
- No migration needed
Team Relocates
Example: Oakland A's → Las Vegas A's (2024-2028)
What happens:
- CloudKit updates
CanonicalTeam:canonicalIdstays"team_mlb_athletics"(never changes)stadiumCanonicalIdupdated to Las Vegas stadiumcityupdated to "Las Vegas"
- CloudKit adds
TeamAliasfor "Oakland" with end date - Old Oakland Coliseum gets
deprecatedAttimestamp (soft delete)
User impact: None
- Old visits preserved:
StadiumVisit.stadiumId= Oakland Coliseum (still valid) - Old visits show historical context: "Oakland A's at Oakland Coliseum"
- Achievements adapt: "Complete AL West" now requires Las Vegas stadium
Stadium Demolished / Team Ceases
What happens:
- Record gets
deprecatedAttimestamp (soft delete, never hard delete) - Filtered from active queries:
predicate { $0.deprecatedAt == nil } - Historical data fully preserved
User impact: None
- Visits remain in history, just not in active stadium lists
- Achievements not revoked - you earned it, you keep it
Identity Resolution
StadiumIdentityService handles all lookups:
// Find canonical ID from any name (current or historical)
StadiumIdentityService.shared.canonicalId(forName: "Staples Center")
// → "stadium_nba_los_angeles_lakers"
// Get current display name from canonical ID
StadiumIdentityService.shared.currentName(forCanonicalId: "stadium_nba_los_angeles_lakers")
// → "Crypto.com Arena"
// Get all historical names
StadiumIdentityService.shared.allNames(forCanonicalId: "stadium_nba_los_angeles_lakers")
// → ["Crypto.com Arena", "Staples Center", "Great Western Forum"]
Sync Safety
During CanonicalSyncService.mergeStadium():
if let existing = try context.fetch(descriptor).first {
// PRESERVE user customizations
let savedNickname = existing.userNickname
let savedFavorite = existing.isFavorite
// UPDATE system fields only
existing.name = remote.name
existing.city = remote.city
// canonicalId is NOT updated - it's immutable
// RESTORE user customizations
existing.userNickname = savedNickname
existing.isFavorite = savedFavorite
}
Impact Summary
| Scenario | User Visits | Achievements | Historical Display |
|---|---|---|---|
| Stadium rename | ✅ Preserved | ✅ Preserved | Shows name at visit time |
| New stadium | N/A | Available to earn | N/A |
| Team relocates | ✅ Preserved | ✅ Logic adapts | Shows team + old stadium |
| Stadium demolished | ✅ Preserved | ✅ Not revoked | Marked deprecated, visible in history |
Key Principle
Immutable references, mutable display. User data stores only canonical IDs. Display names are resolved at read time via StadiumIdentityService. This means we can update the real world without ever migrating user data.