align backend with TSS schema: drop SRE-specific fields, fix cross-tournament queries

- Remove teams_points, clanrating, tag/short/long name, description, region,
  guild_id, clan_id — none of these exist in the TSS DB schema
- Rename long_name → name throughout (TSS teams have one name, not long+short)
- Cross-tournament stat queries now use team_name (string) from player_games_hist
  instead of team_id, since team_id is assigned per-tournament by Spectra
- Leaderboard deduplicates teams_data by name with GROUP BY, MAX(team_id) for roster ref
- team_members roster still uses team_id (correct within a single tournament)
- Fix player_teams_for: was grouping by non-existent team_tag column, now team_id
- Fix games_for: winning_team/losing_team → winning_slot/losing_slot; add mission_name
- Remove joined_unix, points, sqb_points from PlayerSummary; nick resolved from battles_db
- Remove rating_hourly from HistoryResponse (teams_points never existed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
deploy
2026-06-08 01:07:57 +00:00
parent 3436c91fdc
commit 65ad10ad11
+85 -195
View File
@@ -105,51 +105,34 @@ struct SearchResponse {
#[derive(Serialize)]
struct ResolveResponse {
team_id: i64,
long_name: String,
tag_name: Option<String>,
name: String,
}
#[derive(Serialize)]
struct TeamSearchRow {
team_id: i64,
long_name: String,
tag_name: Option<String>,
name: String,
members: i64,
clanrating: Option<i64>,
}
#[derive(Serialize)]
struct TeamLeaderboardRow {
team_id: i64,
clan_id: i64,
long_name: String,
tag_name: Option<String>,
short_name: Option<String>,
name: String,
player_count: i64,
total_battles: i64,
wins: i64,
losses: i64,
win_rate: f64,
total_kills: i64,
points: Points,
}
#[derive(Serialize)]
struct TeamDetail {
team_id: i64,
clan_id: i64,
long_name: String,
tag_name: Option<String>,
short_name: Option<String>,
description: Option<String>,
region: Option<String>,
name: String,
members: i64,
captain_uid: Option<String>,
guild_id: Option<String>,
created_unix: Option<i64>,
updated_unix: Option<i64>,
clanrating: Option<i64>,
data_set: &'static str,
team_summary: TeamSummary,
players: Vec<PlayerSummary>,
@@ -164,12 +147,6 @@ struct TeamSummary {
win_rate: f64,
kdr: f64,
total_kills: i64,
points: Points,
total_points: i64,
}
#[derive(Serialize)]
struct Points {
total_points: i64,
}
@@ -178,9 +155,6 @@ struct PlayerSummary {
uid: String,
nick: Option<String>,
role: String,
joined_unix: Option<i64>,
points: i64,
sqb_points: i64,
total_battles: i64,
wins: i64,
losses: i64,
@@ -196,10 +170,8 @@ struct PlayerSummary {
#[derive(Serialize)]
struct HistoryResponse {
team_id: i64,
long_name: String,
tag_name: Option<String>,
name: String,
history: Vec<PeriodHistory>,
rating_hourly: Vec<RatingPoint>,
}
#[derive(Serialize)]
@@ -211,17 +183,10 @@ struct PeriodHistory {
win_rate: f64,
}
#[derive(Serialize)]
struct RatingPoint {
timestamp: i64,
rating: i64,
}
#[derive(Serialize)]
struct GamesResponse {
team_id: i64,
long_name: String,
tag_name: Option<String>,
name: String,
games: Vec<GameRow>,
}
@@ -290,7 +255,6 @@ struct PlayerCareer {
#[derive(Serialize)]
struct PlayerTeamRef {
team_id: Option<i64>,
team_tag: Option<String>,
team_name: Option<String>,
games: i64,
last_seen: i64,
@@ -298,16 +262,9 @@ struct PlayerTeamRef {
struct TeamRecord {
team_id: i64,
long_name: String,
tag_name: Option<String>,
description: Option<String>,
region: Option<String>,
name: String,
members: i64,
captain_uid: Option<String>,
guild_id: Option<String>,
created_unix: Option<i64>,
updated_unix: Option<i64>,
clanrating: Option<i64>,
}
#[tokio::main]
@@ -411,12 +368,14 @@ async fn leaderboard(
let limit = i64::from(query.limit.unwrap_or(100).clamp(1, 100));
let teams_conn = open_db(&state.teams_db)?;
let battles_conn = open_db(&state.battles_db)?;
// Deduplicate teams by name across tournaments — pick the highest team_id
// (most recent) per name for the roster count, but stats come from team_name.
let mut stmt = teams_conn
.prepare(
"SELECT team_id, long_name, tag_name, description, region, members, captain_uid, guild_id,
created_unix, updated_unix, clanrating
"SELECT MAX(team_id), name, MAX(members), MAX(captain_uid)
FROM teams_data
ORDER BY COALESCE(clanrating, 0) DESC, members DESC, long_name COLLATE NOCASE ASC
GROUP BY name COLLATE NOCASE
ORDER BY MAX(members) DESC, name COLLATE NOCASE ASC
LIMIT ?1",
)
.map_err(db_error)?;
@@ -429,22 +388,16 @@ async fn leaderboard(
let mut rows = Vec::with_capacity(teams.len());
for team in teams {
let summary = team_summary_for(&battles_conn, team.team_id)?;
let summary = team_summary_for(&battles_conn, &team.name)?;
rows.push(TeamLeaderboardRow {
team_id: team.team_id,
clan_id: team.team_id,
long_name: team.long_name.clone(),
tag_name: team.tag_name.clone(),
short_name: team.tag_name.clone(),
name: team.name,
player_count: team.members,
total_battles: summary.total_battles,
wins: summary.wins,
losses: summary.losses,
win_rate: summary.win_rate,
total_kills: summary.total_kills,
points: Points {
total_points: team.clanrating.unwrap_or(summary.total_points),
},
});
}
@@ -460,9 +413,7 @@ async fn resolve_team(
let team = find_team(&conn, name)?.ok_or_else(|| ApiError::not_found("Team not found"))?;
Ok(Json(ResolveResponse {
team_id: team.team_id,
long_name: team.long_name.clone(),
tag_name: team.tag_name.clone(),
name: team.tag_name.clone().unwrap_or(team.long_name),
name: team.name,
}))
}
@@ -477,17 +428,13 @@ async fn search_teams(
let conn = open_db(&state.teams_db)?;
let mut stmt = conn
.prepare(
"SELECT team_id, long_name, tag_name, members, clanrating
"SELECT team_id, name, members
FROM teams_data
WHERE long_name LIKE ?1 ESCAPE '\\' OR tag_name LIKE ?1 ESCAPE '\\'
WHERE name LIKE ?1 ESCAPE '\\'
ORDER BY
CASE
WHEN tag_name = ?2 COLLATE NOCASE THEN 0
WHEN long_name = ?2 COLLATE NOCASE THEN 1
ELSE 2
END,
COALESCE(clanrating, 0) DESC,
long_name COLLATE NOCASE ASC
CASE WHEN name = ?2 COLLATE NOCASE THEN 0 ELSE 1 END,
members DESC,
name COLLATE NOCASE ASC
LIMIT ?3",
)
.map_err(db_error)?;
@@ -496,10 +443,8 @@ async fn search_teams(
.query_map(params![like, name, limit], |row| {
Ok(TeamSearchRow {
team_id: row.get(0)?,
long_name: row.get(1)?,
tag_name: row.get(2)?,
members: row.get(3)?,
clanrating: row.get(4)?,
name: row.get(1)?,
members: row.get(2)?,
})
})
.map_err(db_error)?
@@ -518,23 +463,15 @@ async fn team_detail(
let battles_conn = open_db(&state.battles_db)?;
let team =
find_team(&teams_conn, &decoded)?.ok_or_else(|| ApiError::not_found("Team not found"))?;
let summary = team_summary_for(&battles_conn, team.team_id)?;
let players = player_summaries_for(&teams_conn, &battles_conn, team.team_id)?;
let summary = team_summary_for(&battles_conn, &team.name)?;
// team_id is the most recent tournament entry — used only for the roster lookup.
let players = player_summaries_for(&teams_conn, &battles_conn, team.team_id, &team.name)?;
Ok(Json(TeamDetail {
team_id: team.team_id,
clan_id: team.team_id,
long_name: team.long_name,
tag_name: team.tag_name.clone(),
short_name: team.tag_name,
description: team.description,
region: team.region,
name: team.name,
members: team.members,
captain_uid: team.captain_uid,
guild_id: team.guild_id,
created_unix: team.created_unix,
updated_unix: team.updated_unix,
clanrating: team.clanrating,
data_set: "tss",
team_summary: summary,
players,
@@ -551,15 +488,12 @@ async fn team_history(
let team =
find_team(&teams_conn, &decoded)?.ok_or_else(|| ApiError::not_found("Team not found"))?;
let history = period_history_for(&battles_conn, team.team_id)?;
let rating_hourly = rating_history_for(&teams_conn, team.team_id)?;
let history = period_history_for(&battles_conn, &team.name)?;
Ok(Json(HistoryResponse {
team_id: team.team_id,
long_name: team.long_name,
tag_name: team.tag_name,
name: team.name,
history,
rating_hourly,
}))
}
@@ -572,12 +506,11 @@ async fn team_games(
let battles_conn = open_db(&state.battles_db)?;
let team =
find_team(&teams_conn, &decoded)?.ok_or_else(|| ApiError::not_found("Team not found"))?;
let games = games_for(&battles_conn, team.team_id)?;
let games = games_for(&battles_conn, &team.name)?;
Ok(Json(GamesResponse {
team_id: team.team_id,
long_name: team.long_name,
tag_name: team.tag_name,
name: team.name,
games,
}))
}
@@ -639,33 +572,19 @@ fn db_error(error: rusqlite::Error) -> ApiError {
fn read_team_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<TeamRecord> {
Ok(TeamRecord {
team_id: row.get(0)?,
long_name: row.get(1)?,
tag_name: row.get(2)?,
description: row.get(3)?,
region: row.get(4)?,
members: row.get(5)?,
captain_uid: row.get(6)?,
guild_id: row.get(7)?,
created_unix: row.get(8)?,
updated_unix: row.get(9)?,
clanrating: row.get(10)?,
name: row.get(1)?,
members: row.get(2)?,
captain_uid: row.get(3)?,
})
}
fn find_team(conn: &Connection, name: &str) -> Result<Option<TeamRecord>, ApiError> {
// Return the most recent tournament entry for this team (highest team_id).
conn.query_row(
"SELECT team_id, long_name, tag_name, description, region, members, captain_uid, guild_id,
created_unix, updated_unix, clanrating
"SELECT team_id, name, members, captain_uid
FROM teams_data
WHERE team_id = ?1
OR long_name = ?2 COLLATE NOCASE
OR tag_name = ?2 COLLATE NOCASE
ORDER BY
CASE
WHEN tag_name = ?2 COLLATE NOCASE THEN 0
WHEN long_name = ?2 COLLATE NOCASE THEN 1
ELSE 2
END
WHERE name = ?2 COLLATE NOCASE OR team_id = ?1
ORDER BY team_id DESC
LIMIT 1",
params![name.parse::<i64>().ok(), name],
read_team_record,
@@ -674,7 +593,7 @@ fn find_team(conn: &Connection, name: &str) -> Result<Option<TeamRecord>, ApiErr
.map_err(db_error)
}
fn team_summary_for(conn: &Connection, team_id: i64) -> Result<TeamSummary, ApiError> {
fn team_summary_for(conn: &Connection, team_name: &str) -> Result<TeamSummary, ApiError> {
conn.query_row(
"SELECT
COUNT(DISTINCT session_id),
@@ -687,8 +606,8 @@ fn team_summary_for(conn: &Connection, team_id: i64) -> Result<TeamSummary, ApiE
COALESCE(SUM(score), 0),
COUNT(DISTINCT UID)
FROM player_games_hist
WHERE team_id = ?1",
params![team_id],
WHERE team_name = ?1 COLLATE NOCASE",
params![team_name],
|row| {
let battles: i64 = row.get(0)?;
let wins: i64 = row.get(1)?;
@@ -708,9 +627,6 @@ fn team_summary_for(conn: &Connection, team_id: i64) -> Result<TeamSummary, ApiE
win_rate: percent(wins, battles),
kdr: ratio(total_kills, deaths),
total_kills,
points: Points {
total_points: score + assists + total_kills,
},
total_points: score + assists + total_kills,
})
},
@@ -721,24 +637,24 @@ fn team_summary_for(conn: &Connection, team_id: i64) -> Result<TeamSummary, ApiE
fn player_summaries_for(
teams_conn: &Connection,
battles_conn: &Connection,
// team_id: roster for this specific tournament entry only
team_id: i64,
// team_name: cross-tournament stats key
team_name: &str,
) -> Result<Vec<PlayerSummary>, ApiError> {
let mut stmt = teams_conn
.prepare(
"SELECT uid, nick, role, joined_unix, points
"SELECT uid, role
FROM team_members
WHERE team_id = ?1
ORDER BY points DESC, nick COLLATE NOCASE ASC",
ORDER BY uid",
)
.map_err(db_error)?;
let members = stmt
.query_map(params![team_id], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, Option<String>>(1)?,
row.get::<_, String>(2)?,
row.get::<_, Option<i64>>(3)?,
row.get::<_, i64>(4)?,
row.get::<_, String>(1)?,
))
})
.map_err(db_error)?
@@ -756,15 +672,17 @@ fn player_summaries_for(
COALESCE(SUM(air_kills), 0),
COALESCE(SUM(assists), 0),
COALESCE(SUM(deaths), 0),
COALESCE(SUM(score), 0)
(SELECT nick FROM player_games_hist
WHERE UID = ?2 AND nick IS NOT NULL
ORDER BY endtime_unix DESC LIMIT 1)
FROM player_games_hist
WHERE team_id = ?1 AND UID = ?2",
WHERE team_name = ?1 COLLATE NOCASE AND UID = ?2",
)
.map_err(db_error)?;
for (uid, nick, role, joined_unix, points) in members {
for (uid, role) in members {
let summary = stats_stmt
.query_row(params![team_id, uid], |row| {
.query_row(params![team_name, uid], |row| {
let battles: i64 = row.get(0)?;
let wins: i64 = row.get(1)?;
let losses: i64 = row.get(2)?;
@@ -772,19 +690,12 @@ fn player_summaries_for(
let air: i64 = row.get(4)?;
let assists: i64 = row.get(5)?;
let deaths: i64 = row.get(6)?;
let score: i64 = row.get(7)?;
let nick: Option<String> = row.get(7)?;
let total_kills = ground + air;
Ok(PlayerSummary {
uid: uid.clone(),
nick: nick.clone(),
nick,
role: role.clone(),
joined_unix,
points,
sqb_points: if points == 0 {
score + assists + total_kills
} else {
points
},
total_battles: battles,
wins,
losses,
@@ -801,6 +712,7 @@ fn player_summaries_for(
out.push(summary);
}
out.sort_by(|a, b| b.total_battles.cmp(&a.total_battles));
Ok(out)
}
@@ -931,22 +843,21 @@ fn player_career_for(conn: &Connection, uid: &str) -> Result<Option<PlayerCareer
fn player_teams_for(conn: &Connection, uid: &str) -> Result<Vec<PlayerTeamRef>, ApiError> {
let mut stmt = conn
.prepare(
"SELECT team_tag, MAX(team_name) AS team_name, team_id,
"SELECT team_id, MAX(team_name) AS team_name,
COUNT(DISTINCT session_id) AS games, MAX(endtime_unix) AS last_seen
FROM player_games_hist
WHERE UID = ?1
GROUP BY team_tag
WHERE UID = ?1 AND team_id IS NOT NULL
GROUP BY team_id
ORDER BY last_seen DESC",
)
.map_err(db_error)?;
let teams = stmt
.query_map(params![uid], |row| {
Ok(PlayerTeamRef {
team_tag: row.get(0)?,
team_id: row.get(0)?,
team_name: row.get(1)?,
team_id: row.get(2)?,
games: row.get(3)?,
last_seen: row.get(4)?,
games: row.get(2)?,
last_seen: row.get(3)?,
})
})
.map_err(db_error)?
@@ -955,7 +866,7 @@ fn player_teams_for(conn: &Connection, uid: &str) -> Result<Vec<PlayerTeamRef>,
Ok(teams)
}
fn period_history_for(conn: &Connection, team_id: i64) -> Result<Vec<PeriodHistory>, ApiError> {
fn period_history_for(conn: &Connection, team_name: &str) -> Result<Vec<PeriodHistory>, ApiError> {
let mut stmt = conn
.prepare(
"SELECT
@@ -964,14 +875,14 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result<Vec<PeriodHisto
SUM(CASE WHEN victor_bool = 'Win' THEN 1 ELSE 0 END),
SUM(CASE WHEN victor_bool != 'Win' THEN 1 ELSE 0 END)
FROM player_games_hist
WHERE team_id = ?1 AND endtime_unix > 0
WHERE team_name = ?1 COLLATE NOCASE AND endtime_unix > 0
GROUP BY period
ORDER BY period ASC",
)
.map_err(db_error)?;
let rows = stmt
.query_map(params![team_id], |row| {
.query_map(params![team_name], |row| {
let period: String = row.get(0)?;
let battles: i64 = row.get(1)?;
let wins: i64 = row.get(2)?;
@@ -990,35 +901,13 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result<Vec<PeriodHisto
Ok(rows)
}
fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint>, ApiError> {
let mut stmt = conn
.prepare(
"SELECT unix_time, COALESCE(total_score, 0)
FROM teams_points
WHERE team_id = ?1
ORDER BY unix_time ASC",
)
.map_err(db_error)?;
let rows = stmt
.query_map(params![team_id], |row| {
Ok(RatingPoint {
timestamp: row.get(0)?,
rating: row.get(1)?,
})
})
.map_err(db_error)?
.collect::<Result<Vec<_>, _>>()
.map_err(db_error)?;
Ok(rows)
}
fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError> {
fn games_for(conn: &Connection, team_name: &str) -> Result<Vec<GameRow>, ApiError> {
let mut stmt = conn
.prepare(
"SELECT
p.session_id,
COALESCE(m.endtime_unix, MAX(p.endtime_unix), 0) AS timestamp,
m.mission_name,
m.mission_mode,
CASE
WHEN MAX(CASE WHEN p.victor_bool = 'Win' THEN 1 ELSE 0 END) = 1 THEN 'Win'
@@ -1034,11 +923,11 @@ fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError>
COALESCE(SUM(p.missile_evades), 0),
COALESCE(SUM(p.shell_interceptions), 0),
COALESCE(SUM(p.team_kills_stat), 0),
m.winning_team,
m.losing_team
m.winning_slot,
m.losing_slot
FROM player_games_hist p
LEFT JOIN match_summary m ON m.session_id = p.session_id
WHERE p.team_id = ?1
WHERE p.team_name = ?1 COLLATE NOCASE
GROUP BY p.session_id
ORDER BY timestamp DESC
LIMIT 100",
@@ -1046,30 +935,31 @@ fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError>
.map_err(db_error)?;
let rows = stmt
.query_map(params![team_id], |row| {
.query_map(params![team_name], |row| {
let session_id: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
let mission_mode: Option<String> = row.get(2)?;
let map_name: Option<String> = row.get(2)?;
let mission_mode: Option<String> = row.get(3)?;
Ok(GameRow {
session_id,
timestamp,
endtime_unix: timestamp,
map_name: mission_mode.clone(),
map_name,
mission_mode,
result: row.get(3)?,
player_count: row.get(4)?,
winning_team: row.get(14)?,
losing_team: row.get(15)?,
result: row.get(4)?,
player_count: row.get(5)?,
winning_team: row.get(15)?,
losing_team: row.get(16)?,
stats: GameStats {
ground_kills: row.get(5)?,
air_kills: row.get(6)?,
assists: row.get(7)?,
captures: row.get(8)?,
deaths: row.get(9)?,
score: row.get(10)?,
missile_evades: row.get(11)?,
shell_interceptions: row.get(12)?,
team_kills_stat: row.get(13)?,
ground_kills: row.get(6)?,
air_kills: row.get(7)?,
assists: row.get(8)?,
captures: row.get(9)?,
deaths: row.get(10)?,
score: row.get(11)?,
missile_evades: row.get(12)?,
shell_interceptions: row.get(13)?,
team_kills_stat: row.get(14)?,
},
})
})