from __future__ import annotations import sqlite3 from pathlib import Path from typing import Any, Iterable def connect(db_path: Path) -> sqlite3.Connection: db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(db_path) conn.execute("PRAGMA foreign_keys = ON") return conn def init_db(conn: sqlite3.Connection) -> None: conn.executescript( """ CREATE TABLE IF NOT EXISTS runs ( run_id TEXT PRIMARY KEY, target_date TEXT NOT NULL, created_at TEXT NOT NULL, params_json TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS ranking_entries ( run_id TEXT NOT NULL, ranking INTEGER NOT NULL, character_name TEXT, world_name TEXT, class_name TEXT, sub_class_name TEXT, character_level INTEGER, character_exp INTEGER, character_popularity INTEGER, character_guildname TEXT, UNIQUE(run_id, ranking) ); CREATE TABLE IF NOT EXISTS characters ( ocid TEXT PRIMARY KEY, character_name TEXT, first_seen_at TEXT, last_seen_at TEXT ); CREATE TABLE IF NOT EXISTS equipment_shape_items ( run_id TEXT NOT NULL, ocid TEXT NOT NULL, item_equipment_part TEXT, equipment_slot TEXT, item_name TEXT, item_icon_url TEXT, item_description TEXT, item_shape_name TEXT, item_shape_icon_url TEXT, raw_json TEXT, UNIQUE(run_id, ocid, item_equipment_part, equipment_slot) ); CREATE TABLE IF NOT EXISTS cash_items ( run_id TEXT NOT NULL, ocid TEXT NOT NULL, preset_no INTEGER, cash_item_equipment_part TEXT, cash_item_equipment_slot TEXT, cash_item_name TEXT, cash_item_icon_url TEXT, cash_item_description TEXT, cash_item_label TEXT, date_expire TEXT, date_option_expire TEXT, raw_json TEXT, UNIQUE(run_id, ocid, preset_no, cash_item_equipment_part, cash_item_equipment_slot) ); CREATE TABLE IF NOT EXISTS icon_assets ( url TEXT PRIMARY KEY, sha256 TEXT, local_path TEXT, content_type TEXT, byte_size INTEGER, fetched_at TEXT, error TEXT ); """ ) def fetch_run(conn: sqlite3.Connection, run_id: str) -> dict[str, Any] | None: cursor = conn.execute( "SELECT run_id, target_date, created_at, params_json FROM runs WHERE run_id = ?", (run_id,), ) row = cursor.fetchone() if not row: return None return { "run_id": row[0], "target_date": row[1], "created_at": row[2], "params_json": row[3], } def insert_run( conn: sqlite3.Connection, run_id: str, target_date: str, created_at: str, params_json: str, ) -> None: conn.execute( """ INSERT INTO runs (run_id, target_date, created_at, params_json) VALUES (?, ?, ?, ?) ON CONFLICT(run_id) DO UPDATE SET target_date = excluded.target_date, created_at = excluded.created_at, params_json = excluded.params_json """, (run_id, target_date, created_at, params_json), ) def upsert_ranking_entries( conn: sqlite3.Connection, run_id: str, entries: Iterable[dict[str, Any]], ) -> None: rows = [ ( run_id, entry.get("ranking"), entry.get("character_name"), entry.get("world_name"), entry.get("class_name"), entry.get("sub_class_name"), entry.get("character_level"), entry.get("character_exp"), entry.get("character_popularity"), entry.get("character_guildname"), ) for entry in entries ] conn.executemany( """ INSERT INTO ranking_entries ( run_id, ranking, character_name, world_name, class_name, sub_class_name, character_level, character_exp, character_popularity, character_guildname ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(run_id, ranking) DO UPDATE SET character_name = excluded.character_name, world_name = excluded.world_name, class_name = excluded.class_name, sub_class_name = excluded.sub_class_name, character_level = excluded.character_level, character_exp = excluded.character_exp, character_popularity = excluded.character_popularity, character_guildname = excluded.character_guildname """, rows, ) def upsert_characters( conn: sqlite3.Connection, rows: Iterable[dict[str, Any]], ) -> None: prepared = [ ( row.get("ocid"), row.get("character_name"), row.get("first_seen_at"), row.get("last_seen_at"), ) for row in rows ] conn.executemany( """ INSERT INTO characters (ocid, character_name, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?) ON CONFLICT(ocid) DO UPDATE SET character_name = excluded.character_name, last_seen_at = excluded.last_seen_at """, prepared, ) def upsert_equipment_items( conn: sqlite3.Connection, run_id: str, rows: Iterable[dict[str, Any]], ) -> None: prepared = [ ( run_id, row.get("ocid"), row.get("item_equipment_part"), row.get("equipment_slot"), row.get("item_name"), row.get("item_icon_url"), row.get("item_description"), row.get("item_shape_name"), row.get("item_shape_icon_url"), row.get("raw_json"), ) for row in rows ] conn.executemany( """ INSERT INTO equipment_shape_items ( run_id, ocid, item_equipment_part, equipment_slot, item_name, item_icon_url, item_description, item_shape_name, item_shape_icon_url, raw_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(run_id, ocid, item_equipment_part, equipment_slot) DO UPDATE SET item_name = excluded.item_name, item_icon_url = excluded.item_icon_url, item_description = excluded.item_description, item_shape_name = excluded.item_shape_name, item_shape_icon_url = excluded.item_shape_icon_url, raw_json = excluded.raw_json """, prepared, ) def upsert_cash_items( conn: sqlite3.Connection, run_id: str, rows: Iterable[dict[str, Any]], ) -> None: prepared = [ ( run_id, row.get("ocid"), row.get("preset_no"), row.get("cash_item_equipment_part"), row.get("cash_item_equipment_slot"), row.get("cash_item_name"), row.get("cash_item_icon_url"), row.get("cash_item_description"), row.get("cash_item_label"), row.get("date_expire"), row.get("date_option_expire"), row.get("raw_json"), ) for row in rows ] conn.executemany( """ INSERT INTO cash_items ( run_id, ocid, preset_no, cash_item_equipment_part, cash_item_equipment_slot, cash_item_name, cash_item_icon_url, cash_item_description, cash_item_label, date_expire, date_option_expire, raw_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(run_id, ocid, preset_no, cash_item_equipment_part, cash_item_equipment_slot) DO UPDATE SET cash_item_name = excluded.cash_item_name, cash_item_icon_url = excluded.cash_item_icon_url, cash_item_description = excluded.cash_item_description, cash_item_label = excluded.cash_item_label, date_expire = excluded.date_expire, date_option_expire = excluded.date_option_expire, raw_json = excluded.raw_json """, prepared, ) def fetch_icon_assets( conn: sqlite3.Connection, urls: list[str], ) -> dict[str, dict[str, Any]]: if not urls: return {} placeholders = ",".join(["?"] * len(urls)) query = f"SELECT url, sha256, local_path, error FROM icon_assets WHERE url IN ({placeholders})" cursor = conn.execute(query, urls) result = {} for row in cursor.fetchall(): result[row[0]] = {"sha256": row[1], "local_path": row[2], "error": row[3]} return result def upsert_icon_asset(conn: sqlite3.Connection, record: dict[str, Any]) -> None: conn.execute( """ INSERT INTO icon_assets ( url, sha256, local_path, content_type, byte_size, fetched_at, error ) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(url) DO UPDATE SET sha256 = excluded.sha256, local_path = excluded.local_path, content_type = excluded.content_type, byte_size = excluded.byte_size, fetched_at = excluded.fetched_at, error = excluded.error """, ( record.get("url"), record.get("sha256"), record.get("local_path"), record.get("content_type"), record.get("byte_size"), record.get("fetched_at"), record.get("error"), ), )