use crate::persistence::entity::song_envelope;
use mineral_log::trace;
use mineral_model::{Envelope, SongId};
use sea_orm::sea_query::OnConflict;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set};
use crate::persistence::db::namespace::NamespaceStore;
impl NamespaceStore {
pub async fn put_envelope(
&self,
id: &SongId,
envelope: &Envelope,
) -> crate::persistence::Result<()> {
self.check_source(id.namespace())?;
let Some(db) = self.pool() else {
return Ok(());
};
trace!(target: "persist", song = id.value(), version = envelope.version, "put_envelope");
song_envelope::Entity::insert(song_envelope::ActiveModel {
namespace: Set(self.namespace().to_owned()),
song_value: Set(id.value().to_owned()),
version: Set(i64::from(envelope.version)),
points: Set(envelope.points.clone()),
updated_at: Set(crate::persistence::db::time::now_ms()),
})
.on_conflict(
OnConflict::columns([
song_envelope::Column::Namespace,
song_envelope::Column::SongValue,
])
.update_columns([
song_envelope::Column::Version,
song_envelope::Column::Points,
song_envelope::Column::UpdatedAt,
])
.to_owned(),
)
.exec_without_returning(db)
.await
.map_err(|source| crate::persistence::Error::Record {
operation: "写包络",
record: id.value().to_owned(),
source,
})?;
Ok(())
}
pub async fn get_envelope(
&self,
id: &SongId,
version: u16,
) -> crate::persistence::Result<Option<Envelope>> {
self.check_source(id.namespace())?;
let Some(db) = self.pool() else {
return Ok(None);
};
let row =
song_envelope::Entity::find_by_id((self.namespace().to_owned(), id.value().to_owned()))
.filter(song_envelope::Column::Version.eq(i64::from(version)))
.one(db)
.await
.map_err(|source| crate::persistence::Error::Record {
operation: "读包络",
record: id.value().to_owned(),
source,
})?;
Ok(row.map(|row| Envelope {
points: row.points,
version,
}))
}
}
#[cfg(test)]
mod tests {
use mineral_model::{Envelope, SongId, SourceKind};
use crate::persistence::ServerStore;
#[tokio::test]
async fn envelope_roundtrips() -> color_eyre::Result<()> {
let dir = tempfile::tempdir()?;
let store = ServerStore::open(&dir.path().join("t.db")).await?;
let scope = store.scope(SourceKind::NETEASE);
let id = SongId::new(SourceKind::NETEASE, "s1");
let envelope = Envelope {
points: vec![0, 7, 255, 128],
version: 1,
};
scope.put_envelope(&id, &envelope).await?;
assert_eq!(
scope.get_envelope(&id, 1).await?,
Some(envelope)
);
Ok(())
}
#[tokio::test]
async fn stale_version_reads_as_missing() -> color_eyre::Result<()> {
let dir = tempfile::tempdir()?;
let store = ServerStore::open(&dir.path().join("t.db")).await?;
let scope = store.scope(SourceKind::NETEASE);
let id = SongId::new(SourceKind::NETEASE, "s1");
scope
.put_envelope(
&id,
&Envelope {
points: vec![1, 2, 3],
version: 1,
},
)
.await?;
assert_eq!(scope.get_envelope(&id, 2).await?, None);
Ok(())
}
#[tokio::test]
async fn put_overwrites_previous_row() -> color_eyre::Result<()> {
let dir = tempfile::tempdir()?;
let store = ServerStore::open(&dir.path().join("t.db")).await?;
let scope = store.scope(SourceKind::NETEASE);
let id = SongId::new(SourceKind::NETEASE, "s1");
scope
.put_envelope(
&id,
&Envelope {
points: vec![1, 1, 1],
version: 1,
},
)
.await?;
let renewed = Envelope {
points: vec![9, 9, 9],
version: 2,
};
scope.put_envelope(&id, &renewed).await?;
assert_eq!(scope.get_envelope(&id, 2).await?, Some(renewed));
assert_eq!(
scope.get_envelope(&id, 1).await?,
None,
"旧版本行已被覆盖,不再可读"
);
Ok(())
}
#[tokio::test]
async fn envelope_scoped_by_namespace() -> color_eyre::Result<()> {
let dir = tempfile::tempdir()?;
let store = ServerStore::open(&dir.path().join("t.db")).await?;
let netease = store.scope(SourceKind::NETEASE);
let bilibili = store.scope(SourceKind::BILIBILI);
let id = SongId::new(SourceKind::NETEASE, "s1");
netease
.put_envelope(
&id,
&Envelope {
points: vec![5],
version: 1,
},
)
.await?;
let other = SongId::new(SourceKind::BILIBILI, "s1");
assert_eq!(bilibili.get_envelope(&other, 1).await?, None);
Ok(())
}
#[tokio::test]
async fn disabled_store_is_noop() -> color_eyre::Result<()> {
let scope = ServerStore::disabled().scope(SourceKind::NETEASE);
let id = SongId::new(SourceKind::NETEASE, "s1");
scope
.put_envelope(
&id,
&Envelope {
points: vec![1],
version: 1,
},
)
.await?;
assert_eq!(scope.get_envelope(&id, 1).await?, None);
Ok(())
}
}