zeph_memory/store/
channel_preferences.rs1use zeph_db::sql;
25
26use super::SqliteStore;
27use crate::error::MemoryError;
28
29impl SqliteStore {
30 pub async fn upsert_channel_preference(
46 &self,
47 channel_type: &str,
48 channel_id: &str,
49 key: &str,
50 value: &str,
51 ) -> Result<(), MemoryError> {
52 #[allow(clippy::cast_possible_truncation)]
54 let now_ms = i64::try_from(
55 std::time::SystemTime::now()
56 .duration_since(std::time::UNIX_EPOCH)
57 .unwrap_or_default()
58 .as_millis()
59 .min(i64::MAX as u128),
60 )
61 .unwrap_or(i64::MAX);
62
63 zeph_db::query(sql!(
64 "INSERT INTO channel_preferences \
65 (channel_type, channel_id, pref_key, pref_value, updated_at) \
66 VALUES (?, ?, ?, ?, ?) \
67 ON CONFLICT(channel_type, channel_id, pref_key) DO UPDATE SET \
68 pref_value = excluded.pref_value, \
69 updated_at = excluded.updated_at"
70 ))
71 .bind(channel_type)
72 .bind(channel_id)
73 .bind(key)
74 .bind(value)
75 .bind(now_ms)
76 .execute(&self.pool)
77 .await?;
78
79 Ok(())
80 }
81
82 pub async fn load_channel_preference(
96 &self,
97 channel_type: &str,
98 channel_id: &str,
99 key: &str,
100 ) -> Result<Option<String>, MemoryError> {
101 let row: Option<(String,)> = zeph_db::query_as(sql!(
102 "SELECT pref_value FROM channel_preferences \
103 WHERE channel_type = ? AND channel_id = ? AND pref_key = ?"
104 ))
105 .bind(channel_type)
106 .bind(channel_id)
107 .bind(key)
108 .fetch_optional(&self.pool)
109 .await?;
110
111 Ok(row.map(|(v,)| v))
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 async fn store() -> SqliteStore {
120 SqliteStore::new(":memory:").await.unwrap()
121 }
122
123 #[tokio::test]
124 async fn upsert_and_load_roundtrip() {
125 let s = store().await;
126 s.upsert_channel_preference("cli", "", "provider", "fast")
127 .await
128 .unwrap();
129
130 let val = s
131 .load_channel_preference("cli", "", "provider")
132 .await
133 .unwrap();
134 assert_eq!(val.as_deref(), Some("fast"));
135 }
136
137 #[tokio::test]
138 async fn upsert_overwrites_existing_value() {
139 let s = store().await;
140 s.upsert_channel_preference("cli", "", "provider", "fast")
141 .await
142 .unwrap();
143 s.upsert_channel_preference("cli", "", "provider", "quality")
144 .await
145 .unwrap();
146
147 let val = s
148 .load_channel_preference("cli", "", "provider")
149 .await
150 .unwrap();
151 assert_eq!(val.as_deref(), Some("quality"));
152 }
153
154 #[tokio::test]
155 async fn load_returns_none_when_missing() {
156 let s = store().await;
157 let val = s
158 .load_channel_preference("cli", "", "provider")
159 .await
160 .unwrap();
161 assert!(val.is_none());
162 }
163
164 #[tokio::test]
165 async fn composite_key_is_unique_per_channel_type_and_id() {
166 let s = store().await;
167 s.upsert_channel_preference("cli", "", "provider", "cli-provider")
169 .await
170 .unwrap();
171 s.upsert_channel_preference("tui", "", "provider", "tui-provider")
172 .await
173 .unwrap();
174
175 let cli = s
176 .load_channel_preference("cli", "", "provider")
177 .await
178 .unwrap();
179 let tui = s
180 .load_channel_preference("tui", "", "provider")
181 .await
182 .unwrap();
183 assert_eq!(cli.as_deref(), Some("cli-provider"));
184 assert_eq!(tui.as_deref(), Some("tui-provider"));
185 }
186
187 #[tokio::test]
188 async fn composite_key_is_unique_per_channel_id() {
189 let s = store().await;
190 s.upsert_channel_preference("telegram", "123", "provider", "fast")
192 .await
193 .unwrap();
194 s.upsert_channel_preference("telegram", "456", "provider", "quality")
195 .await
196 .unwrap();
197
198 let chat123 = s
199 .load_channel_preference("telegram", "123", "provider")
200 .await
201 .unwrap();
202 let chat456 = s
203 .load_channel_preference("telegram", "456", "provider")
204 .await
205 .unwrap();
206 assert_eq!(chat123.as_deref(), Some("fast"));
207 assert_eq!(chat456.as_deref(), Some("quality"));
208 }
209
210 #[tokio::test]
211 async fn multiple_keys_per_channel() {
212 let s = store().await;
213 s.upsert_channel_preference("cli", "", "provider", "fast")
214 .await
215 .unwrap();
216 s.upsert_channel_preference("cli", "", "theme", "dark")
217 .await
218 .unwrap();
219
220 let provider = s
221 .load_channel_preference("cli", "", "provider")
222 .await
223 .unwrap();
224 let theme = s.load_channel_preference("cli", "", "theme").await.unwrap();
225 assert_eq!(provider.as_deref(), Some("fast"));
226 assert_eq!(theme.as_deref(), Some("dark"));
227 }
228}