diff --git a/crates/infrastructure/persistence/src/lib.rs b/crates/infrastructure/persistence/src/lib.rs index c789a47..bab8e5c 100644 --- a/crates/infrastructure/persistence/src/lib.rs +++ b/crates/infrastructure/persistence/src/lib.rs @@ -65,26 +65,7 @@ impl SongRepositoryPort for SqliteSongRepository { .await .map_err(|e| RepositoryError::Internal(e.to_string()))?; - rows.into_iter() - .map(|row| { - let id = Uuid::parse_str(&row.id) - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - let preview_chords: Vec = serde_json::from_str(&row.preview_chords) - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - Ok(SongSummary { - id, - meta: SongMeta { - title: row.title, - artist: row.artist, - original_key: row.original_key, - capo: None, - tuning: None, - tempo: None, - }, - preview_chords, - }) - }) - .collect() + rows.into_iter().map(row_to_summary).collect() } async fn get(&self, id: Uuid) -> Result, RepositoryError> { @@ -126,10 +107,11 @@ impl SongRepositoryPort for SqliteSongRepository { #[async_trait] impl SongSearchPort for SqliteSongRepository { async fn search(&self, query: &str) -> Result, RepositoryError> { - let pattern = format!("%{}%", query); + let escaped = query.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_"); + let pattern = format!("%{}%", escaped); let rows = sqlx::query_as::<_, SongRow>( "SELECT id, title, artist, original_key, preview_chords, body FROM songs \ - WHERE title LIKE ? OR artist LIKE ? ORDER BY created_at DESC" + WHERE (title LIKE ? ESCAPE '\\' OR artist LIKE ? ESCAPE '\\') ORDER BY created_at DESC" ) .bind(&pattern) .bind(&pattern) @@ -137,29 +119,29 @@ impl SongSearchPort for SqliteSongRepository { .await .map_err(|e| RepositoryError::Internal(e.to_string()))?; - rows.into_iter() - .map(|row| { - let id = Uuid::parse_str(&row.id) - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - let preview_chords: Vec = serde_json::from_str(&row.preview_chords) - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - Ok(SongSummary { - id, - meta: SongMeta { - title: row.title, - artist: row.artist, - original_key: row.original_key, - capo: None, - tuning: None, - tempo: None, - }, - preview_chords, - }) - }) - .collect() + rows.into_iter().map(row_to_summary).collect() } } +fn row_to_summary(row: SongRow) -> Result { + let id = Uuid::parse_str(&row.id) + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + let preview_chords: Vec = serde_json::from_str(&row.preview_chords) + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(SongSummary { + id, + meta: SongMeta { + title: row.title, + artist: row.artist, + original_key: row.original_key, + capo: None, + tuning: None, + tempo: None, + }, + preview_chords, + }) +} + pub struct SqliteRepositoryFactory; impl SqliteRepositoryFactory {