use async_trait::async_trait; use domain::{DomainError, SongSearchPort, SongSummary, SortField, SortOrder}; use crate::repository::SqliteSongRepository; use crate::row::{SongRow, row_to_summary, sort_clause}; #[async_trait] impl SongSearchPort for SqliteSongRepository { async fn search( &self, query: &str, sort: SortField, order: SortOrder, ) -> Result, DomainError> { let escaped = query .replace('\\', "\\\\") .replace('%', "\\%") .replace('_', "\\_"); let pattern = format!("%{}%", escaped); let sql = format!( "SELECT id, title, artist, original_key, preview_chords, body FROM songs \ WHERE (title LIKE ? ESCAPE '\\' OR artist LIKE ? ESCAPE '\\') {}", sort_clause(sort, order) ); let rows = sqlx::query_as::<_, SongRow>(&sql) .bind(&pattern) .bind(&pattern) .fetch_all(&self.pool) .await .map_err(|e| DomainError::InfrastructureError(e.to_string()))?; rows.into_iter().map(row_to_summary).collect() } }