use zeph_db::sql;
use super::SqliteStore;
use crate::error::MemoryError;
impl SqliteStore {
pub async fn upsert_channel_preference(
&self,
channel_type: &str,
channel_id: &str,
key: &str,
value: &str,
) -> Result<(), MemoryError> {
#[allow(clippy::cast_possible_truncation)]
let now_ms = i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.min(i64::MAX as u128),
)
.unwrap_or(i64::MAX);
zeph_db::query(sql!(
"INSERT INTO channel_preferences \
(channel_type, channel_id, pref_key, pref_value, updated_at) \
VALUES (?, ?, ?, ?, ?) \
ON CONFLICT(channel_type, channel_id, pref_key) DO UPDATE SET \
pref_value = excluded.pref_value, \
updated_at = excluded.updated_at"
))
.bind(channel_type)
.bind(channel_id)
.bind(key)
.bind(value)
.bind(now_ms)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn load_channel_preference(
&self,
channel_type: &str,
channel_id: &str,
key: &str,
) -> Result<Option<String>, MemoryError> {
let row: Option<(String,)> = zeph_db::query_as(sql!(
"SELECT pref_value FROM channel_preferences \
WHERE channel_type = ? AND channel_id = ? AND pref_key = ?"
))
.bind(channel_type)
.bind(channel_id)
.bind(key)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(|(v,)| v))
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn store() -> SqliteStore {
SqliteStore::new(":memory:").await.unwrap()
}
#[tokio::test]
async fn upsert_and_load_roundtrip() {
let s = store().await;
s.upsert_channel_preference("cli", "", "provider", "fast")
.await
.unwrap();
let val = s
.load_channel_preference("cli", "", "provider")
.await
.unwrap();
assert_eq!(val.as_deref(), Some("fast"));
}
#[tokio::test]
async fn upsert_overwrites_existing_value() {
let s = store().await;
s.upsert_channel_preference("cli", "", "provider", "fast")
.await
.unwrap();
s.upsert_channel_preference("cli", "", "provider", "quality")
.await
.unwrap();
let val = s
.load_channel_preference("cli", "", "provider")
.await
.unwrap();
assert_eq!(val.as_deref(), Some("quality"));
}
#[tokio::test]
async fn load_returns_none_when_missing() {
let s = store().await;
let val = s
.load_channel_preference("cli", "", "provider")
.await
.unwrap();
assert!(val.is_none());
}
#[tokio::test]
async fn composite_key_is_unique_per_channel_type_and_id() {
let s = store().await;
s.upsert_channel_preference("cli", "", "provider", "cli-provider")
.await
.unwrap();
s.upsert_channel_preference("tui", "", "provider", "tui-provider")
.await
.unwrap();
let cli = s
.load_channel_preference("cli", "", "provider")
.await
.unwrap();
let tui = s
.load_channel_preference("tui", "", "provider")
.await
.unwrap();
assert_eq!(cli.as_deref(), Some("cli-provider"));
assert_eq!(tui.as_deref(), Some("tui-provider"));
}
#[tokio::test]
async fn composite_key_is_unique_per_channel_id() {
let s = store().await;
s.upsert_channel_preference("telegram", "123", "provider", "fast")
.await
.unwrap();
s.upsert_channel_preference("telegram", "456", "provider", "quality")
.await
.unwrap();
let chat123 = s
.load_channel_preference("telegram", "123", "provider")
.await
.unwrap();
let chat456 = s
.load_channel_preference("telegram", "456", "provider")
.await
.unwrap();
assert_eq!(chat123.as_deref(), Some("fast"));
assert_eq!(chat456.as_deref(), Some("quality"));
}
#[tokio::test]
async fn multiple_keys_per_channel() {
let s = store().await;
s.upsert_channel_preference("cli", "", "provider", "fast")
.await
.unwrap();
s.upsert_channel_preference("cli", "", "theme", "dark")
.await
.unwrap();
let provider = s
.load_channel_preference("cli", "", "provider")
.await
.unwrap();
let theme = s.load_channel_preference("cli", "", "theme").await.unwrap();
assert_eq!(provider.as_deref(), Some("fast"));
assert_eq!(theme.as_deref(), Some("dark"));
}
}