use rusqlite::{params, Connection}; pub fn init_db(conn: &Connection) { conn.execute( "CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, hashed_password TEXT NOT NULL, account_id INTEGER UNIQUE, balance INTEGER DEFAULT 1000 )", [], ) .unwrap(); // Add column if not exists (for migration) let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []); let _ = conn.execute( "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000", [], ); conn.execute( "CREATE TABLE IF NOT EXISTS plays ( session_id TEXT PRIMARY KEY, username TEXT NOT NULL, game_type TEXT NOT NULL, bet_amount INTEGER NOT NULL, winnings INTEGER NOT NULL, balance_after INTEGER NOT NULL, details TEXT NOT NULL DEFAULT '{}', created_at DATETIME DEFAULT CURRENT_TIMESTAMP )", [], ) .unwrap(); } pub fn add_user( conn: &Connection, username: &str, hashed_password: &str, ) -> Result<(), Box> { if get_hashed_password(conn, username).is_some() { return Err("user already exists".into()); } for _ in 0..10 { let account_id: u32 = rand::random::(); match conn.execute( "INSERT INTO users (username, hashed_password, account_id, balance) VALUES (?1, ?2, ?3, ?4)", params![username, hashed_password, account_id as i64, 1000i64], ) { Ok(_) => return Ok(()), Err(rusqlite::Error::SqliteFailure(error, _)) if error.code == rusqlite::ErrorCode::ConstraintViolation => { continue; } Err(error) => return Err(Box::new(error)), } } Err("could not allocate a unique account id".into()) } pub fn get_hashed_password(conn: &Connection, username: &str) -> Option { let mut stmt = conn .prepare("SELECT hashed_password FROM users WHERE username = ?1") .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { row.get(0).ok() } else { None } } pub fn set_hashed_password( conn: &Connection, username: &str, hashed_password: &str, ) -> rusqlite::Result<()> { conn.execute( "UPDATE users SET hashed_password = ?1 WHERE username = ?2", params![hashed_password, username], )?; Ok(()) } pub fn get_account_id(conn: &Connection, username: &str) -> Option { let mut stmt = conn .prepare("SELECT account_id FROM users WHERE username = ?1") .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { Some(row.get::<_, i64>(0).ok()?) } else { None } } pub fn get_balance(conn: &Connection, username: &str) -> Option { let mut stmt = conn .prepare("SELECT balance FROM users WHERE username = ?1") .unwrap(); let mut rows = stmt.query([username]).unwrap(); if let Ok(Some(row)) = rows.next() { Some(row.get::<_, i64>(0).ok()?) } else { None } } /// Settle one play and record its audit row in a single transaction. /// /// `details` is a JSON object with game-specific fields (reels, cards, /// winning number, crash point, ...). All SQL is parameterized: usernames are /// data, never part of the statement text. /// /// Returns the balance after the play. #[allow(clippy::too_many_arguments)] pub fn apply_play( conn: &mut Connection, session_id: &str, username: &str, game_type: &str, bet_amount: i64, winnings: i64, details: &str, ) -> rusqlite::Result { let tx = conn.transaction()?; let deducted = tx.execute( "UPDATE users SET balance = balance - ?1 WHERE username = ?2 AND balance >= ?1", params![bet_amount, username], )?; if deducted != 1 { return Err(rusqlite::Error::QueryReturnedNoRows); } tx.execute( "UPDATE users SET balance = balance + ?1 WHERE username = ?2", params![winnings, username], )?; let balance_after: i64 = tx.query_row( "SELECT balance FROM users WHERE username = ?1", [username], |row| row.get(0), )?; tx.execute( "INSERT INTO plays (session_id, username, game_type, bet_amount, winnings, balance_after, details) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![ session_id, username, game_type, bet_amount, winnings, balance_after, details ], )?; tx.commit()?; Ok(balance_after) } pub fn count_plays(conn: &Connection, username: &str) -> i64 { conn.query_row( "SELECT COUNT(*) FROM plays WHERE username = ?1", [username], |row| row.get(0), ) .unwrap_or(0) }