fix(persistence): extract row_to_summary helper, escape LIKE wildcards

This commit is contained in:
2026-04-08 03:38:12 +02:00
parent 377fe957bc
commit 6a3cdbe4c3

View File

@@ -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<String> = 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<Option<Song>, RepositoryError> {
@@ -126,10 +107,11 @@ impl SongRepositoryPort for SqliteSongRepository {
#[async_trait]
impl SongSearchPort for SqliteSongRepository {
async fn search(&self, query: &str) -> Result<Vec<SongSummary>, 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<String> = 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<SongSummary, RepositoryError> {
let id = Uuid::parse_str(&row.id)
.map_err(|e| RepositoryError::Internal(e.to_string()))?;
let preview_chords: Vec<String> = 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 {