From 775b905069b60c5a7916969579d33f01500a42b6 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Fri, 1 May 2026 19:56:42 +0000 Subject: [PATCH] schema: add canonical tournament & participant tables Adds tournaments, participants (canonical), tournament_results, and participant_surface_elos (canonical). Adds nullable tournament_id to scoring_events and nullable participant_id to season_participants. Phase 1b of canonical tournament layer migration. Co-Authored-By: Claude Opus 4.7 --- database/schema.ts | 155 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/database/schema.ts b/database/schema.ts index 6fa32f0..f763fca 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -150,6 +150,12 @@ export const simulatorTypeEnum = pgEnum("simulator_type", [ "llws_bracket", ]); +export const tournamentStatusEnum = pgEnum("tournament_status", [ + "scheduled", + "in_progress", + "completed", +]); + export const playoffMatchGameStatusEnum = pgEnum("playoff_match_game_status", [ "scheduled", "complete", @@ -378,6 +384,9 @@ export const seasonParticipants = pgTable("season_participants", { sportsSeasonId: uuid("sports_season_id") .notNull() .references(() => sportsSeasons.id, { onDelete: "cascade" }), + participantId: uuid("participant_id").references(() => participants.id, { + onDelete: "restrict", + }), name: varchar("name", { length: 255 }).notNull(), shortName: varchar("short_name", { length: 100 }), externalId: varchar("external_id", { length: 255 }), @@ -389,6 +398,96 @@ export const seasonParticipants = pgTable("season_participants", { uniqueIndex("participants_sports_season_name_unique").on(table.sportsSeasonId, table.name), ]); +export const participants = pgTable( + "participants", + { + id: uuid("id").primaryKey().defaultRandom(), + sportId: uuid("sport_id") + .notNull() + .references(() => sports.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + externalKey: varchar("external_key", { length: 255 }), + metadata: jsonb("metadata"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => ({ + uniqueSportName: uniqueIndex("participants_sport_name_unique").on( + t.sportId, + t.name, + ), + }), +); + +export const tournaments = pgTable( + "tournaments", + { + id: uuid("id").primaryKey().defaultRandom(), + sportId: uuid("sport_id") + .notNull() + .references(() => sports.id, { onDelete: "cascade" }), + name: varchar("name", { length: 255 }).notNull(), + year: integer("year").notNull(), + startsAt: timestamp("starts_at"), + endsAt: timestamp("ends_at"), + surface: varchar("surface", { length: 50 }), // tennis only: hard | clay | grass + location: varchar("location", { length: 255 }), + status: tournamentStatusEnum("status").notNull().default("scheduled"), + externalKey: varchar("external_key", { length: 255 }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => ({ + uniqueSportNameYear: uniqueIndex("tournaments_sport_name_year_unique").on( + t.sportId, + t.name, + t.year, + ), + }), +); + +export const tournamentResults = pgTable( + "tournament_results", + { + id: uuid("id").primaryKey().defaultRandom(), + tournamentId: uuid("tournament_id") + .notNull() + .references(() => tournaments.id, { onDelete: "cascade" }), + participantId: uuid("participant_id") + .notNull() + .references(() => participants.id, { onDelete: "cascade" }), + placement: integer("placement"), + rawScore: decimal("raw_score", { precision: 10, scale: 2 }), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => ({ + uniqueTournamentParticipant: uniqueIndex( + "tournament_results_tournament_participant_unique", + ).on(t.tournamentId, t.participantId), + }), +); + +export const participantSurfaceElos = pgTable( + "participant_surface_elos", + { + id: uuid("id").primaryKey().defaultRandom(), + participantId: uuid("participant_id") + .notNull() + .references(() => participants.id, { onDelete: "cascade" }), + worldRanking: integer("world_ranking"), + eloHard: integer("elo_hard"), + eloClay: integer("elo_clay"), + eloGrass: integer("elo_grass"), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (t) => ({ + uniqueParticipant: uniqueIndex("participant_surface_elos_participant_unique").on( + t.participantId, + ), + }), +); + export const seasonTemplateSports = pgTable("season_template_sports", { id: uuid("id").primaryKey().defaultRandom(), templateId: uuid("template_id") @@ -435,6 +534,9 @@ export const scoringEvents = pgTable("scoring_events", { sportsSeasonId: uuid("sports_season_id") .notNull() .references(() => sportsSeasons.id, { onDelete: "cascade" }), + tournamentId: uuid("tournament_id").references(() => tournaments.id, { + onDelete: "set null", + }), name: varchar("name", { length: 255 }).notNull(), // "2024 Masters", "Super Bowl LIX", etc. eventDate: date("event_date"), eventStartsAt: timestamp("event_starts_at", { withTimezone: true }), @@ -783,6 +885,8 @@ export const teamEvSnapshots = pgTable("team_ev_snapshots", { // Relations export const sportsRelations = relations(sports, ({ many }) => ({ sportsSeasons: many(sportsSeasons), + tournaments: many(tournaments), + participants: many(participants), })); export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({ @@ -803,10 +907,57 @@ export const seasonParticipantsRelations = relations(seasonParticipants, ({ one, fields: [seasonParticipants.sportsSeasonId], references: [sportsSeasons.id], }), + canonicalParticipant: one(participants, { + fields: [seasonParticipants.participantId], + references: [participants.id], + }), results: many(seasonParticipantResults), regularSeasonStandings: many(regularSeasonStandings), })); +export const participantsRelations = relations(participants, ({ one, many }) => ({ + sport: one(sports, { + fields: [participants.sportId], + references: [sports.id], + }), + seasonParticipants: many(seasonParticipants), + tournamentResults: many(tournamentResults), + surfaceElo: one(participantSurfaceElos, { + fields: [participants.id], + references: [participantSurfaceElos.participantId], + }), +})); + +export const tournamentsRelations = relations(tournaments, ({ one, many }) => ({ + sport: one(sports, { + fields: [tournaments.sportId], + references: [sports.id], + }), + scoringEvents: many(scoringEvents), + tournamentResults: many(tournamentResults), +})); + +export const tournamentResultsRelations = relations(tournamentResults, ({ one }) => ({ + tournament: one(tournaments, { + fields: [tournamentResults.tournamentId], + references: [tournaments.id], + }), + participant: one(participants, { + fields: [tournamentResults.participantId], + references: [participants.id], + }), +})); + +export const participantSurfaceElosRelations = relations( + participantSurfaceElos, + ({ one }) => ({ + participant: one(participants, { + fields: [participantSurfaceElos.participantId], + references: [participants.id], + }), + }), +); + export const seasonTemplatesRelations = relations(seasonTemplates, ({ many }) => ({ seasonTemplateSports: many(seasonTemplateSports), seasons: many(seasons), @@ -943,6 +1094,10 @@ export const scoringEventsRelations = relations(scoringEvents, ({ one, many }) = fields: [scoringEvents.sportsSeasonId], references: [sportsSeasons.id], }), + tournament: one(tournaments, { + fields: [scoringEvents.tournamentId], + references: [tournaments.id], + }), eventResults: many(eventResults), playoffMatches: many(playoffMatches), tournamentGroups: many(tournamentGroups),