tss db wipe and update (#1305)

This commit is contained in:
NotSoToothless
2026-06-07 17:28:16 -07:00
committed by GitHub
parent 632a441080
commit abab7ea9fa
7 changed files with 223 additions and 396 deletions
+17 -26
View File
@@ -15,7 +15,7 @@ from typing import Any, Optional
import discord
from . import preferences, storage
from . import preferences
log = logging.getLogger("tssbot.autolog")
@@ -33,18 +33,21 @@ def set_bot(bot: discord.Client) -> None:
_bot = bot
def _present_team_tags(game: dict[str, Any]) -> set[str]:
"""Return the set of team tags present in a game dict."""
players = game.get("players") or {}
tags: set[str] = set()
for p in players.values():
if not isinstance(p, dict):
def _present_teams(game: dict[str, Any]) -> tuple[set[str], set[str]]:
"""Return stable TSS team IDs and names embedded in a replay."""
tss = game.get("tss") or {}
team_ids: set[str] = set()
team_names: set[str] = set()
for slot in ("1", "2"):
team = tss.get(slot)
if not isinstance(team, dict):
continue
tag_raw = p.get("tag") or ""
tag = tag_raw[1:-1] if len(tag_raw) > 2 else tag_raw
if tag:
tags.add(tag)
return tags
if team.get("team_id") is not None:
team_ids.add(str(team["team_id"]))
name = str(team.get("team_name") or team.get("name") or "").strip()
if name:
team_names.add(name.lower())
return team_ids, team_names
async def process_game(game: dict[str, Any]) -> None:
@@ -59,19 +62,7 @@ async def process_game(game: dict[str, Any]) -> None:
if not session_id:
return
present_tags = _present_team_tags(game)
tags_lower = {t.lower() for t in present_tags}
# Resolve present tags → team_ids so team subscriptions (keyed by team_id) match.
present_team_ids: set[str] = set()
for tag in present_tags:
try:
tid = await storage.resolve_team_id_for_tag(tag)
except Exception as exc:
log.error("tag resolve failed for %s: %s", tag, exc)
tid = None
if tid is not None:
present_team_ids.add(str(tid))
present_team_ids, present_team_names = _present_teams(game)
sent = _sent_channels_by_session.setdefault(session_id, set())
@@ -84,7 +75,7 @@ async def process_game(game: dict[str, Any]) -> None:
continue
name = (entry.get("Name") or "").lower()
matched = str(entity_id) in present_team_ids or (name and name in tags_lower)
matched = str(entity_id) in present_team_ids or (name and name in present_team_names)
if not matched or channel_id in sent:
continue
+6 -6
View File
@@ -58,7 +58,7 @@ async def team_autocomplete(interaction: discord.Interaction, current: str) -> L
return []
choices = []
for r in rows:
label = (r.get("tag_name") or r.get("long_name") or str(r["team_id"]))[:100]
label = (r.get("long_name") or str(r["team_id"]))[:100]
choices.append(app_commands.Choice(name=label, value=str(r["team_id"])))
return choices
@@ -112,7 +112,7 @@ class TssCommands(commands.Cog):
# ── /set-team ──────────────────────────────────────────────────────────
@app_commands.command(name="set-team", description="Set this server's team")
@app_commands.describe(team="Team name, tag, or ID")
@app_commands.describe(team="TSS team name or ID")
@app_commands.autocomplete(team=team_autocomplete)
@app_commands.checks.has_permissions(manage_guild=True)
@not_blacklisted()
@@ -123,7 +123,7 @@ class TssCommands(commands.Cog):
resolved = await storage.resolve_team(team)
if not resolved:
return await interaction.followup.send(f"Could not find a team matching **{team}**.", ephemeral=True)
name = resolved.get("tag_name") or resolved.get("long_name") or str(resolved["team_id"])
name = resolved.get("long_name") or str(resolved["team_id"])
preferences.set_guild_team(interaction.guild_id, int(resolved["team_id"]), name)
await interaction.followup.send(
f"✅ This server's team is now **{name}** (id `{resolved['team_id']}`).", ephemeral=True
@@ -131,7 +131,7 @@ class TssCommands(commands.Cog):
# ── /log-team ──────────────────────────────────────────────────────────
@app_commands.command(name="log-team", description="Send a team's matches to this channel")
@app_commands.describe(team="Team name, tag, or ID")
@app_commands.describe(team="TSS team name or ID")
@app_commands.autocomplete(team=team_autocomplete)
@app_commands.checks.has_permissions(manage_guild=True)
@not_blacklisted()
@@ -142,7 +142,7 @@ class TssCommands(commands.Cog):
resolved = await storage.resolve_team(team)
if not resolved:
return await interaction.followup.send(f"Could not find a team matching **{team}**.", ephemeral=True)
name = resolved.get("tag_name") or resolved.get("long_name") or str(resolved["team_id"])
name = resolved.get("long_name") or str(resolved["team_id"])
preferences.upsert_log_entry(
interaction.guild_id, int(resolved["team_id"]), "tss-team", name, interaction.channel_id
)
@@ -223,7 +223,7 @@ class TssCommands(commands.Cog):
embed.add_field(
name="Teams seen with",
value="\n".join(
f"`{(tm.get('team_tag') or '?')}` — {tm.get('games') or 0} games" for tm in top
f"`{(tm.get('team_name') or '?')}` — {tm.get('games') or 0} games" for tm in top
),
inline=False,
)
+198 -131
View File
@@ -2,7 +2,7 @@
Two databases live under ``STORAGE_VOL_PATH`` (set in ``TSSBOT/.env``):
* ``tss_teams.db`` — persistent team registry
* ``tss_teams.db`` — replay-sourced TSS team registry
* ``tss_battles.db`` — per-match summary + per-player/per-vehicle game history
One row is written to ``player_games_hist`` per vehicle *actually used* by each
@@ -51,8 +51,10 @@ TSS_TEAMS_DB_PATH: Path = STORAGE_DIR / "tss_teams.db"
_MATCH_SUMMARY_SQL = """
CREATE TABLE IF NOT EXISTS match_summary (
session_id TEXT PRIMARY KEY,
map_name TEXT,
mission_mode TEXT,
mission_name TEXT,
level_path TEXT,
mission_path TEXT,
difficulty TEXT,
starttime_unix INTEGER,
endtime_unix INTEGER,
@@ -61,14 +63,19 @@ CREATE TABLE IF NOT EXISTS match_summary (
winning_slot TEXT,
losing_slot TEXT,
received_unix INTEGER,
tournament_id INTEGER
tournament_id INTEGER,
tournament_name TEXT,
match_id TEXT,
bracket TEXT
)
"""
_MATCH_SUMMARY_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_ms_map_name ON match_summary(map_name)",
"CREATE INDEX IF NOT EXISTS idx_ms_endtime ON match_summary(endtime_unix)",
"CREATE INDEX IF NOT EXISTS idx_ms_difficulty ON match_summary(difficulty)",
"CREATE INDEX IF NOT EXISTS idx_ms_mission_name ON match_summary(mission_name)",
"CREATE INDEX IF NOT EXISTS idx_ms_level_path ON match_summary(level_path)",
"CREATE INDEX IF NOT EXISTS idx_ms_mission_path ON match_summary(mission_path)",
"CREATE INDEX IF NOT EXISTS idx_ms_endtime ON match_summary(endtime_unix)",
"CREATE INDEX IF NOT EXISTS idx_ms_difficulty ON match_summary(difficulty)",
]
@@ -77,7 +84,6 @@ CREATE TABLE IF NOT EXISTS player_games_hist (
UID TEXT NOT NULL,
nick TEXT NOT NULL,
team_name TEXT,
team_tag TEXT NOT NULL DEFAULT 'UNKNOWN',
team_slot TEXT,
session_id TEXT NOT NULL,
vehicle TEXT,
@@ -95,9 +101,7 @@ CREATE TABLE IF NOT EXISTS player_games_hist (
victor_bool TEXT NOT NULL DEFAULT 'Loss',
endtime_unix INTEGER NOT NULL DEFAULT 0,
team_id INTEGER,
tss_team_uuid TEXT,
tss_role TEXT,
tss_place INTEGER,
pvp_ratio REAL,
UNIQUE (UID, session_id, vehicle_internal)
)
@@ -106,7 +110,6 @@ CREATE TABLE IF NOT EXISTS player_games_hist (
_PLAYER_GAMES_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_pgh_session ON player_games_hist(session_id)",
"CREATE INDEX IF NOT EXISTS idx_pgh_uid_time ON player_games_hist(UID, endtime_unix)",
"CREATE INDEX IF NOT EXISTS idx_pgh_team_tag_time ON player_games_hist(team_tag, endtime_unix)",
"CREATE INDEX IF NOT EXISTS idx_pgh_team_name_time ON player_games_hist(team_name, endtime_unix)",
"CREATE INDEX IF NOT EXISTS idx_pgh_endtime ON player_games_hist(endtime_unix)",
"CREATE INDEX IF NOT EXISTS idx_pgh_nick ON player_games_hist(nick COLLATE NOCASE)",
@@ -122,25 +125,15 @@ _PLAYER_GAMES_INDEXES = [
_TEAMS_DATA_SQL = """
CREATE TABLE IF NOT EXISTS teams_data (
team_id INTEGER PRIMARY KEY AUTOINCREMENT,
long_name TEXT NOT NULL UNIQUE,
short_name TEXT UNIQUE,
tag_name TEXT,
description TEXT,
region TEXT,
team_id INTEGER PRIMARY KEY,
long_name TEXT NOT NULL,
members INTEGER NOT NULL DEFAULT 0,
members_json TEXT,
captain_uid TEXT,
guild_id TEXT,
clanrating INTEGER,
created_unix INTEGER,
updated_unix INTEGER
captain_uid TEXT
)
"""
_TEAMS_DATA_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_teams_data_tag_name ON teams_data(tag_name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS idx_teams_data_guild_id ON teams_data(guild_id)",
"CREATE INDEX IF NOT EXISTS idx_teams_data_long_name ON teams_data(long_name COLLATE NOCASE)",
]
@@ -148,10 +141,7 @@ _TEAM_MEMBERS_SQL = """
CREATE TABLE IF NOT EXISTS team_members (
team_id INTEGER NOT NULL,
uid TEXT NOT NULL,
nick TEXT,
role TEXT NOT NULL DEFAULT 'player',
points INTEGER NOT NULL DEFAULT 0,
joined_unix INTEGER,
PRIMARY KEY (team_id, uid)
)
"""
@@ -177,22 +167,6 @@ _TEAM_NAME_HISTORY_INDEXES = [
]
_TEAMS_POINTS_SQL = """
CREATE TABLE IF NOT EXISTS teams_points (
team_id INTEGER NOT NULL,
long_name TEXT,
unix_time INTEGER NOT NULL,
total_score INTEGER,
PRIMARY KEY (team_id, unix_time)
)
"""
_TEAMS_POINTS_INDEXES = [
"CREATE INDEX IF NOT EXISTS idx_teams_points_long_name ON teams_points(long_name COLLATE NOCASE)",
"CREATE INDEX IF NOT EXISTS idx_teams_points_unix_time ON teams_points(unix_time)",
]
# ---------------------------------------------------------------------------
# Init
# ---------------------------------------------------------------------------
@@ -215,19 +189,31 @@ async def _existing_columns(conn: aiosqlite.Connection, table: str) -> set[str]:
return {row[1] for row in rows}
async def _migrate(
conn: aiosqlite.Connection, table: str, additions: dict[str, str]
async def _rebuild_table(
conn: aiosqlite.Connection,
table: str,
create_sql: str,
desired_columns: list[str],
) -> None:
"""Add any missing columns. `additions` maps column name → full column DDL.
"""Rebuild a table when its schema contains obsolete or missing columns."""
existing = await _existing_columns(conn, table)
desired = set(desired_columns)
if existing == desired:
return
SQLite's ``ALTER TABLE ADD COLUMN`` only accepts a literal DEFAULT, which
is fine for all our additions. Existing rows pick up the DEFAULT value.
"""
cols = await _existing_columns(conn, table)
for col, ddl in additions.items():
if col not in cols:
await conn.execute(f"ALTER TABLE {table} ADD COLUMN {ddl}")
log.info("migrated %s: added column %s", table, col)
old_table = f"{table}_old_schema"
await conn.execute(f"DROP TABLE IF EXISTS {old_table}")
await conn.execute(f"ALTER TABLE {table} RENAME TO {old_table}")
await conn.execute(create_sql)
common = [column for column in desired_columns if column in existing]
if common:
columns = ", ".join(common)
await conn.execute(
f"INSERT OR IGNORE INTO {table} ({columns}) "
f"SELECT {columns} FROM {old_table}"
)
await conn.execute(f"DROP TABLE {old_table}")
log.info("migrated %s: rebuilt replay-native schema", table)
async def _init_battles_db() -> None:
@@ -236,17 +222,22 @@ async def _init_battles_db() -> None:
await conn.execute(sql)
await conn.execute(_MATCH_SUMMARY_SQL)
await conn.execute(_PLAYER_GAMES_SQL)
await _rebuild_table(conn, "match_summary", _MATCH_SUMMARY_SQL, [
"session_id", "mission_mode", "mission_name", "level_path",
"mission_path", "difficulty",
"starttime_unix", "endtime_unix", "duration", "draw",
"winning_slot", "losing_slot", "received_unix", "tournament_id",
"tournament_name", "match_id", "bracket",
])
await _rebuild_table(conn, "player_games_hist", _PLAYER_GAMES_SQL, [
"UID", "nick", "team_name", "team_slot", "session_id",
"vehicle", "vehicle_internal", "ground_kills", "air_kills",
"assists", "captures", "deaths", "score", "missile_evades",
"shell_interceptions", "team_kills_stat", "country_id",
"victor_bool", "endtime_unix", "team_id", "tss_role", "pvp_ratio",
])
await _apply(conn, _MATCH_SUMMARY_INDEXES)
await _apply(conn, _PLAYER_GAMES_INDEXES)
await _migrate(conn, "match_summary", {
"tournament_id": "tournament_id INTEGER",
})
await _migrate(conn, "player_games_hist", {
"tss_team_uuid": "tss_team_uuid TEXT",
"tss_role": "tss_role TEXT",
"tss_place": "tss_place INTEGER",
"pvp_ratio": "pvp_ratio REAL",
})
await conn.commit()
@@ -257,23 +248,17 @@ async def _init_teams_db() -> None:
await conn.execute(_TEAMS_DATA_SQL)
await conn.execute(_TEAM_MEMBERS_SQL)
await conn.execute(_TEAM_NAME_HISTORY_SQL)
await conn.execute(_TEAMS_POINTS_SQL)
# Forward-only migrations for DBs created before a column landed.
# Keep CREATE TABLE statements above in sync — these only matter when
# the table already existed.
await _migrate(conn, "teams_data", {
"clanrating": "clanrating INTEGER",
})
await _migrate(conn, "team_members", {
"points": "points INTEGER NOT NULL DEFAULT 0",
})
await conn.execute("DROP TABLE IF EXISTS teams_points")
await _rebuild_table(conn, "teams_data", _TEAMS_DATA_SQL, [
"team_id", "long_name", "members", "captain_uid",
])
await _rebuild_table(conn, "team_members", _TEAM_MEMBERS_SQL, [
"team_id", "uid", "role",
])
await _apply(conn, _TEAMS_DATA_INDEXES)
await _apply(conn, _TEAM_MEMBERS_INDEXES)
await _apply(conn, _TEAM_NAME_HISTORY_INDEXES)
await _apply(conn, _TEAMS_POINTS_INDEXES)
await conn.commit()
@@ -300,23 +285,44 @@ async def insert_match(game: Dict[str, Any]) -> None:
"""Insert one row into match_summary from a normalised game dict.
``game["_id"]`` must already be a hex string (normalised by tss_ws).
Safe to call multiple times — INSERT OR IGNORE skips duplicates.
Safe to call multiple times; newer replay metadata refreshes the row.
"""
async with aiosqlite.connect(TSS_BATTLES_DB_PATH) as conn:
for sql in _PRAGMAS:
await conn.execute(sql)
tss = game.get("tss") or {}
await conn.execute(
"""
INSERT OR IGNORE INTO match_summary
(session_id, map_name, mission_mode, difficulty,
INSERT INTO match_summary
(session_id, mission_mode, mission_name, level_path, mission_path,
difficulty,
starttime_unix, endtime_unix, duration,
draw, winning_slot, losing_slot, received_unix)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
draw, winning_slot, losing_slot, received_unix,
tournament_id, tournament_name, match_id, bracket)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id) DO UPDATE SET
mission_mode = excluded.mission_mode,
mission_name = excluded.mission_name,
level_path = excluded.level_path,
mission_path = excluded.mission_path,
difficulty = excluded.difficulty,
starttime_unix = excluded.starttime_unix,
endtime_unix = excluded.endtime_unix,
duration = excluded.duration,
draw = excluded.draw,
winning_slot = excluded.winning_slot,
losing_slot = excluded.losing_slot,
tournament_id = excluded.tournament_id,
tournament_name = excluded.tournament_name,
match_id = excluded.match_id,
bracket = excluded.bracket
""",
(
str(game["_id"]),
str(game.get("mission_name") or game.get("level_path") or ""),
str(game.get("mission_mode") or ""),
str(game.get("mission_name") or ""),
str(game.get("level_path") or ""),
str(game.get("mission_path") or ""),
str(game.get("difficulty") or ""),
int(game.get("start_ts") or 0),
int(game.get("end_ts") or 0),
@@ -325,6 +331,10 @@ async def insert_match(game: Dict[str, Any]) -> None:
str(game.get("winner") or ""),
str(game.get("loser") or ""),
int(time.time()),
tss.get("tournament_id"),
str(tss.get("tournament_name") or ""),
str(tss.get("match_id") or ""),
str(tss.get("bracket") or ""),
),
)
await conn.commit()
@@ -335,18 +345,30 @@ async def insert_player_games(game: Dict[str, Any]) -> None:
victor_bool is set to 'Win' when the player's team slot matches the
winning slot, 'Loss' otherwise.
Safe to call multiple times — INSERT OR IGNORE skips duplicates.
Safe to call multiple times; newer replay metadata refreshes existing rows.
"""
session_id = str(game["_id"])
winner_slot = str(game.get("winner") or "")
end_ts = int(game.get("end_ts") or 0)
players = game.get("players") or {}
tss = game.get("tss") or {}
tss_players: dict[str, dict[str, Any]] = {}
tss_teams: dict[str, dict[str, Any]] = {}
for slot in ("1", "2"):
team = tss.get(slot)
if not isinstance(team, dict):
continue
tss_teams[slot] = team
for member in team.get("players") or []:
if isinstance(member, dict) and member.get("uid") is not None:
tss_players[str(member["uid"])] = member
rows = []
for uid_str, p in players.items():
victor_bool = "Win" if str(p.get("team", "")) == winner_slot else "Loss"
tag_raw = p.get("tag") or ""
team_tag = tag_raw[1:-1] if len(tag_raw) > 2 else tag_raw
tss_team = tss_teams.get(str(p.get("team") or ""), {})
tss_player = tss_players.get(str(uid_str), {})
used_units = [u for u in (p.get("units") or []) if u.get("used")]
if not used_units:
@@ -356,8 +378,7 @@ async def insert_player_games(game: Dict[str, Any]) -> None:
rows.append((
str(uid_str),
str(p.get("name") or ""),
"", # team_name — resolved later
team_tag,
str(tss_team.get("team_name") or tss_team.get("name") or ""),
str(p.get("team") or ""), # team_slot ("1" or "2")
session_id,
str(unit.get("unit_normalized") or ""),
@@ -374,7 +395,9 @@ async def insert_player_games(game: Dict[str, Any]) -> None:
p.get("country_id"),
victor_bool,
end_ts,
None, # team_id — resolved later
tss_team.get("team_id"),
str(tss_player.get("role") or ""),
tss_player.get("pvp_ratio"),
))
if not rows:
@@ -385,29 +408,98 @@ async def insert_player_games(game: Dict[str, Any]) -> None:
await conn.execute(sql)
await conn.executemany(
"""
INSERT OR IGNORE INTO player_games_hist
(UID, nick, team_name, team_tag, team_slot, session_id,
INSERT INTO player_games_hist
(UID, nick, team_name, team_slot, session_id,
vehicle, vehicle_internal,
ground_kills, air_kills, assists, captures, deaths, score,
missile_evades, shell_interceptions, team_kills_stat,
country_id, victor_bool, endtime_unix, team_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
country_id, victor_bool, endtime_unix, team_id, tss_role, pvp_ratio)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(UID, session_id, vehicle_internal) DO UPDATE SET
nick = excluded.nick,
team_name = excluded.team_name,
team_slot = excluded.team_slot,
vehicle = excluded.vehicle,
ground_kills = excluded.ground_kills,
air_kills = excluded.air_kills,
assists = excluded.assists,
captures = excluded.captures,
deaths = excluded.deaths,
score = excluded.score,
missile_evades = excluded.missile_evades,
shell_interceptions = excluded.shell_interceptions,
team_kills_stat = excluded.team_kills_stat,
country_id = excluded.country_id,
victor_bool = excluded.victor_bool,
endtime_unix = excluded.endtime_unix,
team_id = excluded.team_id,
tss_role = excluded.tss_role,
pvp_ratio = excluded.pvp_ratio
""",
rows,
)
await conn.commit()
async def upsert_tss_teams(game: Dict[str, Any]) -> None:
"""Upsert replay-sourced team identity, roster, and name history."""
tss = game.get("tss") or {}
seen_unix = int(game.get("end_ts") or time.time())
async with aiosqlite.connect(TSS_TEAMS_DB_PATH) as conn:
for sql in _PRAGMAS:
await conn.execute(sql)
for slot in ("1", "2"):
team = tss.get(slot)
if not isinstance(team, dict) or team.get("team_id") is None:
continue
team_id = int(team["team_id"])
team_name = str(team.get("team_name") or team.get("name") or "")
if not team_name:
continue
members = [
member for member in (team.get("players") or [])
if isinstance(member, dict) and member.get("uid") is not None
]
await conn.execute(
"""
INSERT INTO teams_data (team_id, long_name, members, captain_uid)
VALUES (?, ?, ?, ?)
ON CONFLICT(team_id) DO UPDATE SET
long_name = excluded.long_name,
members = excluded.members,
captain_uid = excluded.captain_uid
""",
(team_id, team_name, len(members), str(team.get("captain_id") or "")),
)
await conn.execute(
"""
INSERT INTO team_name_history (team_id, long_name, first_seen, last_seen)
VALUES (?, ?, ?, ?)
ON CONFLICT(team_id, long_name) DO UPDATE SET last_seen = excluded.last_seen
""",
(team_id, team_name, seen_unix, seen_unix),
)
if "players" in team:
await conn.execute("DELETE FROM team_members WHERE team_id = ?", (team_id,))
for member in members:
await conn.execute(
"""
INSERT INTO team_members (team_id, uid, role)
VALUES (?, ?, ?)
ON CONFLICT(team_id, uid) DO UPDATE SET role = excluded.role
""",
(team_id, str(member["uid"]), str(member.get("role") or "player")),
)
await conn.commit()
# ---------------------------------------------------------------------------
# Team resolve / lookup (python twin of the Rust backend's find_team)
# ---------------------------------------------------------------------------
async def resolve_team(name_or_id: str) -> Optional[Dict[str, Any]]:
"""Resolve a team by numeric id, or by tag/short/long name (case-insensitive).
Returns ``{team_id, long_name, short_name, tag_name}`` or None.
Tag matches rank above short, then long name.
"""
"""Resolve a team by numeric ID or full TSS team name."""
text = (name_or_id or "").strip()
if not text:
return None
@@ -420,18 +512,10 @@ async def resolve_team(name_or_id: str) -> Optional[Dict[str, Any]]:
conn.row_factory = aiosqlite.Row
async with conn.execute(
"""
SELECT team_id, long_name, short_name, tag_name
SELECT team_id, long_name
FROM teams_data
WHERE team_id = ?1
OR long_name = ?2 COLLATE NOCASE
OR short_name = ?2 COLLATE NOCASE
OR tag_name = ?2 COLLATE NOCASE
ORDER BY CASE
WHEN tag_name = ?2 COLLATE NOCASE THEN 0
WHEN short_name = ?2 COLLATE NOCASE THEN 1
WHEN long_name = ?2 COLLATE NOCASE THEN 2
ELSE 3
END
LIMIT 1
""",
(as_id, text),
@@ -440,30 +524,16 @@ async def resolve_team(name_or_id: str) -> Optional[Dict[str, Any]]:
return dict(row) if row else None
async def resolve_team_id_for_tag(tag: str) -> Optional[int]:
"""Return the team_id whose tag_name matches ``tag`` (case-insensitive)."""
tag = (tag or "").strip()
if not tag:
return None
async with aiosqlite.connect(TSS_TEAMS_DB_PATH) as conn:
async with conn.execute(
"SELECT team_id FROM teams_data WHERE tag_name = ? COLLATE NOCASE LIMIT 1",
(tag,),
) as cur:
row = await cur.fetchone()
return int(row[0]) if row else None
async def search_teams(query: str, limit: int = 25) -> List[Dict[str, Any]]:
"""Autocomplete-friendly team search. Empty query → top teams by rating."""
"""Autocomplete-friendly TSS team-name search."""
q = (query or "").strip()
async with aiosqlite.connect(TSS_TEAMS_DB_PATH) as conn:
conn.row_factory = aiosqlite.Row
if not q:
async with conn.execute(
"""
SELECT team_id, long_name, tag_name FROM teams_data
ORDER BY clanrating DESC NULLS LAST, members DESC
SELECT team_id, long_name FROM teams_data
ORDER BY members DESC, long_name COLLATE NOCASE
LIMIT ?
""",
(limit,),
@@ -473,13 +543,11 @@ async def search_teams(query: str, limit: int = 25) -> List[Dict[str, Any]]:
like = f"%{q}%"
async with conn.execute(
"""
SELECT team_id, long_name, tag_name FROM teams_data
SELECT team_id, long_name FROM teams_data
WHERE long_name LIKE ?1 COLLATE NOCASE
OR tag_name LIKE ?1 COLLATE NOCASE
ORDER BY CASE
WHEN tag_name = ?2 COLLATE NOCASE THEN 0
WHEN long_name = ?2 COLLATE NOCASE THEN 1
ELSE 2
WHEN long_name = ?2 COLLATE NOCASE THEN 0
ELSE 1
END
LIMIT ?3
""",
@@ -603,14 +671,13 @@ async def player_teams(uid: str) -> List[Dict[str, Any]]:
conn.row_factory = aiosqlite.Row
async with conn.execute(
"""
SELECT team_tag,
SELECT team_id,
MAX(team_name) AS team_name,
team_id,
COUNT(DISTINCT session_id) AS games,
MAX(endtime_unix) AS last_seen
FROM player_games_hist
WHERE UID = ?
GROUP BY team_tag
GROUP BY team_id, team_name
ORDER BY last_seen DESC
""",
(uid,),