//! User persistence. use sqlx::PgPool; use uuid::Uuid; use crate::domain::User; use crate::error::{AppError, AppResult}; pub async fn create(pool: &PgPool, username: &str, password_hash: &str) -> AppResult { let result = sqlx::query_as::<_, User>( r#" INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id, username, password_hash, created_at "#, ) .bind(username) .bind(password_hash) .fetch_one(pool) .await; match result { Ok(user) => Ok(user), Err(e) if is_unique_violation(&e) => { Err(AppError::Conflict("username is already taken".into())) } Err(e) => Err(AppError::Database(e)), } } /// Case-insensitive lookup so login with "Alice" matches a user /// registered as "alice" (the unique index on `lower(username)` keeps /// the comparison cheap). Equivalent in spirit to ILIKE but uses the /// functional index directly. pub async fn find_by_username(pool: &PgPool, username: &str) -> AppResult> { let row = sqlx::query_as::<_, User>( r#" SELECT id, username, password_hash, created_at FROM users WHERE lower(username) = lower($1) "#, ) .bind(username) .fetch_optional(pool) .await?; Ok(row) } pub async fn find_by_id(pool: &PgPool, id: Uuid) -> AppResult> { let row = sqlx::query_as::<_, User>( r#"SELECT id, username, password_hash, created_at FROM users WHERE id = $1"#, ) .bind(id) .fetch_optional(pool) .await?; Ok(row) } fn is_unique_violation(err: &sqlx::Error) -> bool { if let sqlx::Error::Database(db_err) = err { db_err.code().as_deref() == Some("23505") } else { false } }