1use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
3use sqlx::Error;
4
5#[derive(Debug, Clone)]
6pub struct GameRecord {
7 pub id: i64,
8 pub name: String,
9 pub status: String,
10 pub white_player: String,
11 pub black_player: String,
12}
13
14pub struct DbClient {
15 pool: SqlitePool,
16}
17
18impl DbClient {
19 pub async fn new(database_url: &str) -> Result<Self, Error> {
20 use sqlx::sqlite::SqliteConnectOptions;
21 use std::str::FromStr;
22
23 let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
24
25 let pool = SqlitePoolOptions::new()
26 .max_connections(5)
27 .connect_with(options)
28 .await?;
29
30 sqlx::migrate!("./migrations").run(&pool).await?;
32
33 Ok(Self { pool })
34 }
35
36 pub async fn create_player(&self, name: &str) -> Result<i64, Error> {
37 let result = sqlx::query!("INSERT INTO players (name) VALUES (?)", name)
38 .execute(&self.pool)
39 .await?;
40 Ok(result.last_insert_rowid())
41 }
42
43 pub async fn get_or_create_player(&self, name: &str) -> Result<i64, Error> {
44 if let Some(id) = self.get_player_by_name(name).await? {
45 return Ok(id);
46 }
47 self.create_player(name).await
48 }
49
50 pub async fn get_player_by_name(&self, name: &str) -> Result<Option<i64>, Error> {
51 let record = sqlx::query!("SELECT id FROM players WHERE name = ?", name)
52 .fetch_optional(&self.pool)
53 .await?;
54 Ok(record.and_then(|r| r.id)) }
56
57 pub async fn create_game(
58 &self,
59 name: &str,
60 white_id: i64,
61 black_id: i64,
62 initial_fen: &str,
63 ) -> Result<i64, Error> {
64 let status = "ongoing";
65 let result = sqlx::query!(
66 "INSERT INTO games (name, white_player_id, black_player_id, current_fen, status) VALUES (?, ?, ?, ?, ?)",
67 name, white_id, black_id, initial_fen, status
68 )
69 .execute(&self.pool)
70 .await?;
71 Ok(result.last_insert_rowid())
72 }
73
74 pub async fn update_game_state(
75 &self,
76 game_id: i64,
77 current_fen: &str,
78 status: &str,
79 ) -> Result<(), Error> {
80 sqlx::query!(
81 "UPDATE games SET current_fen = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
82 current_fen, status, game_id
83 )
84 .execute(&self.pool)
85 .await?;
86 Ok(())
87 }
88
89 pub async fn set_flip_board(&self, game_id: i64, flipped: bool) -> Result<(), Error> {
91 sqlx::query!(
92 "UPDATE games SET flip_board = ? WHERE id = ?",
93 flipped,
94 game_id
95 )
96 .execute(&self.pool)
97 .await?;
98 Ok(())
99 }
100
101 pub async fn get_flip_board(&self, game_id: i64) -> Result<bool, Error> {
102 let record = sqlx::query!("SELECT flip_board FROM games WHERE id = ?", game_id)
103 .fetch_optional(&self.pool)
104 .await?;
105 Ok(record.is_some_and(|r| r.flip_board != 0))
106 }
107
108 pub async fn delete_game(&self, game_id: i64) -> Result<(), Error> {
109 let mut tx = self.pool.begin().await?;
110
111 sqlx::query!("DELETE FROM chat_messages WHERE game_id = ?", game_id)
113 .execute(&mut *tx)
114 .await?;
115 sqlx::query!("DELETE FROM moves WHERE game_id = ?", game_id)
116 .execute(&mut *tx)
117 .await?;
118 sqlx::query!("DELETE FROM games WHERE id = ?", game_id)
119 .execute(&mut *tx)
120 .await?;
121
122 tx.commit().await?;
123 Ok(())
124 }
125
126 pub async fn log_move(
127 &self,
128 game_id: i64,
129 move_number: i64,
130 fen_snapshot: &str,
131 notation: &str,
132 ) -> Result<(), Error> {
133 sqlx::query!(
134 "INSERT INTO moves (game_id, move_number, fen_snapshot, notation) VALUES (?, ?, ?, ?)",
135 game_id,
136 move_number,
137 fen_snapshot,
138 notation
139 )
140 .execute(&self.pool)
141 .await?;
142 Ok(())
143 }
144
145 pub async fn undo_last_move(&self, game_id: i64) -> Result<Option<String>, Error> {
147 let last_move = sqlx::query!(
149 "SELECT id, move_number FROM moves WHERE game_id = ? ORDER BY move_number DESC LIMIT 1",
150 game_id
151 )
152 .fetch_optional(&self.pool)
153 .await?;
154
155 if let Some(lm) = last_move {
156 sqlx::query!("DELETE FROM moves WHERE id = ?", lm.id)
158 .execute(&self.pool)
159 .await?;
160
161 let prev_move = sqlx::query!(
163 "SELECT fen_snapshot FROM moves WHERE game_id = ? ORDER BY move_number DESC LIMIT 1",
164 game_id
165 )
166 .fetch_optional(&self.pool)
167 .await?;
168
169 let recovered_fen = prev_move.map(|r| r.fen_snapshot).unwrap_or_else(|| {
170 "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".to_string()
172 });
173
174 sqlx::query!(
176 "UPDATE games SET current_fen = ?, status = 'ongoing', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
177 recovered_fen, game_id
178 )
179 .execute(&self.pool)
180 .await?;
181
182 return Ok(Some(recovered_fen));
183 }
184
185 Ok(None)
186 }
187
188 pub async fn get_active_games(&self) -> Result<Vec<GameRecord>, sqlx::Error> {
190 let records = sqlx::query!(
191 r#"
192 SELECT
193 g.id, g.name, g.status,
194 pw.name as white_name,
195 pb.name as black_name
196 FROM games g
197 JOIN players pw ON g.white_player_id = pw.id
198 JOIN players pb ON g.black_player_id = pb.id
199 ORDER BY g.updated_at DESC
200 "#
201 )
202 .fetch_all(&self.pool)
203 .await?;
204
205 Ok(records
206 .into_iter()
207 .map(|rec| GameRecord {
208 id: rec.id,
209 name: rec.name,
210 status: rec.status,
211 white_player: rec.white_name,
212 black_player: rec.black_name,
213 })
214 .collect())
215 }
216
217 pub async fn load_game_history(
219 &self,
220 game_id: i64,
221 ) -> Result<(String, Vec<String>, Vec<String>), sqlx::Error> {
222 let root_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".to_string();
224
225 let moves = sqlx::query!(
226 "SELECT fen_snapshot, notation FROM moves WHERE game_id = ? ORDER BY move_number ASC",
227 game_id
228 )
229 .fetch_all(&self.pool)
230 .await?;
231
232 let mut fen_history = Vec::new();
233 let mut algebraic_history = Vec::new();
234
235 for r in moves {
236 fen_history.push(r.fen_snapshot);
237 algebraic_history.push(r.notation);
238 }
239
240 Ok((root_fen, fen_history, algebraic_history))
241 }
242
243 pub async fn log_chat_message(
244 &self,
245 game_id: i64,
246 role: &str,
247 content: &str,
248 ) -> Result<i64, Error> {
249 let result = sqlx::query!(
250 "INSERT INTO chat_messages (game_id, role, content) VALUES (?, ?, ?)",
251 game_id,
252 role,
253 content
254 )
255 .execute(&self.pool)
256 .await?;
257 Ok(result.last_insert_rowid())
258 }
259
260 pub async fn load_chat_history(&self, game_id: i64) -> Result<Vec<(String, String)>, Error> {
261 let messages = sqlx::query!(
262 "SELECT role, content FROM chat_messages WHERE game_id = ? ORDER BY id ASC",
263 game_id
264 )
265 .fetch_all(&self.pool)
266 .await?;
267
268 let mut chat_history = Vec::new();
269 for r in messages {
270 chat_history.push((r.role, r.content));
271 }
272
273 Ok(chat_history)
274 }
275}