use std::collections::HashMap;
use rusqlite::{Connection, OptionalExtension, params};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UidKind {
Artist,
Album,
Track,
Playlist,
}
impl UidKind {
fn table(self) -> &'static str {
match self {
Self::Artist => "artists",
Self::Album => "albums",
Self::Track => "tracks",
Self::Playlist => "playlists",
}
}
}
pub fn is_uid(s: &str) -> bool {
s.len() == 36 && uuid::Uuid::try_parse(s).is_ok_and(|u| u.get_version_num() == 7)
}
pub fn resolve_id(conn: &Connection, kind: UidKind, raw: &str) -> rusqlite::Result<Option<i64>> {
if let Ok(id) = raw.parse() {
return Ok(Some(id));
}
if !is_uid(raw) {
return Ok(None);
}
id_for_uid(conn, kind, raw)
}
pub fn uids_in_order(
conn: &Connection,
kind: UidKind,
ids: &[i64],
) -> rusqlite::Result<Vec<String>> {
let uids = uids_for(conn, kind, ids.iter().copied())?;
Ok(ids
.iter()
.map(|id| uids.get(id).cloned().unwrap_or_else(|| id.to_string()))
.collect())
}
pub fn adopt_uid(
conn: &Connection,
kind: UidKind,
id: i64,
remote_id: &str,
) -> rusqlite::Result<()> {
if !is_uid(remote_id) {
return Ok(());
}
let table = kind.table();
let adopted = conn
.prepare_cached(&format!(
"UPDATE {table} SET uid = ?1
WHERE id = ?2 AND uid IS NOT ?1
AND NOT EXISTS (SELECT 1 FROM {table} WHERE uid = ?1)"
))?
.execute(params![remote_id, id])?;
if adopted == 0
&& let Some(other) = id_for_uid(conn, kind, remote_id)?.filter(|other| *other != id)
{
log::warn!(
"{table} {id}: server id {remote_id} is already row {other}'s; kept its own uid"
);
}
Ok(())
}
pub fn uids_for(
conn: &Connection,
kind: UidKind,
ids: impl IntoIterator<Item = i64>,
) -> rusqlite::Result<HashMap<i64, String>> {
let ids = serde_json::to_string(&ids.into_iter().collect::<Vec<_>>()).unwrap_or_default();
let mut stmt = conn.prepare_cached(&format!(
"SELECT id, uid FROM {} WHERE id IN (SELECT value FROM json_each(?1)) AND uid IS NOT NULL",
kind.table()
))?;
stmt.query_map([ids], |r| Ok((r.get(0)?, r.get(1)?)))?
.collect()
}
pub fn id_for_uid(conn: &Connection, kind: UidKind, uid: &str) -> rusqlite::Result<Option<i64>> {
conn.prepare_cached(&format!("SELECT id FROM {} WHERE uid = ?1", kind.table()))?
.query_row(params![uid], |r| r.get(0))
.optional()
}
pub fn find_uid(conn: &Connection, uid: &str) -> rusqlite::Result<Option<(UidKind, i64)>> {
for kind in [UidKind::Track, UidKind::Album, UidKind::Artist] {
if let Some(id) = id_for_uid(conn, kind, uid)? {
return Ok(Some((kind, id)));
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_uid_names_one_row_of_one_kind() {
let conn = Connection::open_in_memory().unwrap();
crate::db::schema::create_tables(&conn).unwrap();
conn.execute_batch(
"INSERT INTO artists (id, name) VALUES (5, 'Burial');
INSERT INTO albums (id, title, artist_id) VALUES (5, 'Untrue', 5);
INSERT INTO tracks (id, title, album_id, artist_id) VALUES (5, 'Archangel', 5, 5);",
)
.unwrap();
let album = uids_for(&conn, UidKind::Album, [5]).unwrap()[&5].clone();
let track = uids_for(&conn, UidKind::Track, [5]).unwrap()[&5].clone();
assert_ne!(album, track);
assert_eq!(find_uid(&conn, &album).unwrap(), Some((UidKind::Album, 5)));
assert_eq!(find_uid(&conn, &track).unwrap(), Some((UidKind::Track, 5)));
assert_eq!(id_for_uid(&conn, UidKind::Artist, &album).unwrap(), None);
assert_eq!(resolve_id(&conn, UidKind::Album, &album).unwrap(), Some(5));
assert_eq!(resolve_id(&conn, UidKind::Album, "5").unwrap(), Some(5));
assert_eq!(resolve_id(&conn, UidKind::Album, &track).unwrap(), None);
assert_eq!(resolve_id(&conn, UidKind::Album, "al-5").unwrap(), None);
}
#[test]
fn a_server_uid_another_row_holds_stays_with_it() {
let conn = Connection::open_in_memory().unwrap();
crate::db::schema::create_tables(&conn).unwrap();
conn.execute_batch(
"INSERT INTO artists (id, name) VALUES (1, 'Burial');
INSERT INTO albums (id, title, artist_id) VALUES (1, 'Untrue', 1);
INSERT INTO tracks (id, title, album_id, artist_id) VALUES
(1, 'Archangel', 1, 1), (2, 'Archangel', 1, 1);",
)
.unwrap();
let server = uuid::Uuid::now_v7().to_string();
adopt_uid(&conn, UidKind::Track, 1, &server).unwrap();
let own = uids_for(&conn, UidKind::Track, [2]).unwrap()[&2].clone();
adopt_uid(&conn, UidKind::Track, 2, &server).unwrap();
let uids = uids_for(&conn, UidKind::Track, [1, 2]).unwrap();
assert_eq!(uids[&1], server);
assert_eq!(uids[&2], own);
}
#[test]
fn only_a_hyphenated_uuid_v7_is_a_uid() {
assert!(is_uid(&uuid::Uuid::now_v7().to_string()));
assert!(!is_uid(&uuid::Uuid::now_v7().simple().to_string()));
assert!(!is_uid("0f8fad5b-d9cb-469f-a165-70867728950e"));
assert!(!is_uid("mf-5"));
assert!(!is_uid("5"));
}
}