1use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use uuid::Uuid;
7
8use crate::types::{BatchWriteSummary, DeleteMode, Page, PageRequest, SqlValue, StorageResult};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct Note {
13 pub id: Uuid,
14 pub namespace: String,
15 pub kind: String,
16 pub status: String,
17 pub name: Option<String>,
18 pub content: String,
19 pub salience: Option<f64>,
20 pub decay_factor: Option<f64>,
21 pub expires_at: Option<i64>,
22 pub properties: Option<Value>,
23 pub created_at: i64,
24 pub updated_at: i64,
25 pub deleted_at: Option<i64>,
26}
27
28impl Note {
29 pub fn new(
31 namespace: impl Into<String>,
32 kind: impl Into<String>,
33 content: impl Into<String>,
34 ) -> Self {
35 let now = chrono::Utc::now().timestamp_micros();
36 Self {
37 id: Uuid::new_v4(),
38 namespace: namespace.into(),
39 kind: kind.into(),
40 status: "active".to_string(),
41 name: None,
42 content: content.into(),
43 salience: None,
44 decay_factor: None,
45 expires_at: None,
46 properties: None,
47 created_at: now,
48 updated_at: now,
49 deleted_at: None,
50 }
51 }
52
53 pub fn with_name(mut self, n: impl Into<String>) -> Self {
55 self.name = Some(n.into());
56 self
57 }
58
59 pub fn with_salience(mut self, s: f64) -> Self {
63 if !s.is_finite() {
64 return self;
65 }
66 self.salience = Some(s.clamp(0.0, 1.0));
67 self
68 }
69
70 pub fn with_decay(mut self, d: f64) -> Self {
74 if !d.is_finite() {
75 return self;
76 }
77 self.decay_factor = Some(d.max(0.0));
78 self
79 }
80
81 pub fn try_with_salience(mut self, s: f64) -> Result<Self, String> {
84 if !s.is_finite() {
85 return Err(format!("salience must be finite, got {s}"));
86 }
87 if !(0.0..=1.0).contains(&s) {
88 return Err(format!("salience must be in [0.0, 1.0], got {s}"));
89 }
90 self.salience = Some(s);
91 Ok(self)
92 }
93
94 pub fn try_with_decay(mut self, d: f64) -> Result<Self, String> {
97 if !d.is_finite() {
98 return Err(format!("decay_factor must be finite, got {d}"));
99 }
100 if d < 0.0 {
101 return Err(format!("decay_factor must be >= 0.0, got {d}"));
102 }
103 self.decay_factor = Some(d);
104 Ok(self)
105 }
106
107 pub fn with_properties(mut self, p: Value) -> Self {
109 self.properties = Some(p);
110 self
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn base_note() -> Note {
119 Note::new("ns:test", "memory", "hello world")
120 }
121
122 #[test]
125 fn with_salience_clamps_to_range() {
126 let n = base_note().with_salience(1.5);
127 assert_eq!(n.salience, Some(1.0));
128 let n = base_note().with_salience(-0.1);
129 assert_eq!(n.salience, Some(0.0));
130 let n = base_note().with_salience(0.7);
131 assert_eq!(n.salience, Some(0.7));
132 }
133
134 #[test]
135 fn with_salience_ignores_nan() {
136 let n = base_note().with_salience(f64::NAN);
137 assert_eq!(n.salience, None, "NaN must not set salience");
138 }
139
140 #[test]
141 fn with_salience_ignores_inf() {
142 let n = base_note().with_salience(f64::INFINITY);
143 assert_eq!(n.salience, None, "+Inf must not set salience");
144 let n = base_note().with_salience(f64::NEG_INFINITY);
145 assert_eq!(n.salience, None, "-Inf must not set salience");
146 }
147
148 #[test]
151 fn with_decay_floors_at_zero() {
152 let n = base_note().with_decay(-1.0);
153 assert_eq!(n.decay_factor, Some(0.0));
154 let n = base_note().with_decay(0.5);
155 assert_eq!(n.decay_factor, Some(0.5));
156 }
157
158 #[test]
159 fn with_decay_ignores_nan() {
160 let n = base_note().with_decay(f64::NAN);
161 assert_eq!(n.decay_factor, None, "NaN must not set decay_factor");
162 }
163
164 #[test]
165 fn with_decay_ignores_inf() {
166 let n = base_note().with_decay(f64::INFINITY);
167 assert_eq!(n.decay_factor, None, "+Inf must not set decay_factor");
168 }
169
170 #[test]
173 fn try_with_salience_accepts_valid_range() {
174 let n = base_note().try_with_salience(0.0).unwrap();
175 assert_eq!(n.salience, Some(0.0));
176 let n = base_note().try_with_salience(1.0).unwrap();
177 assert_eq!(n.salience, Some(1.0));
178 let n = base_note().try_with_salience(0.85).unwrap();
179 assert_eq!(n.salience, Some(0.85));
180 }
181
182 #[test]
183 fn try_with_salience_rejects_nan() {
184 let err = base_note().try_with_salience(f64::NAN).unwrap_err();
185 assert!(err.contains("finite"), "error must mention finite: {err}");
186 }
187
188 #[test]
189 fn try_with_salience_rejects_out_of_range() {
190 let err = base_note().try_with_salience(1.1).unwrap_err();
191 assert!(err.contains("1.0"), "error must mention bound: {err}");
192 let err = base_note().try_with_salience(-0.01).unwrap_err();
193 assert!(err.contains("0.0"), "error must mention bound: {err}");
194 }
195
196 #[test]
199 fn try_with_decay_accepts_valid_values() {
200 let n = base_note().try_with_decay(0.0).unwrap();
201 assert_eq!(n.decay_factor, Some(0.0));
202 let n = base_note().try_with_decay(2.5).unwrap();
203 assert_eq!(n.decay_factor, Some(2.5));
204 }
205
206 #[test]
207 fn try_with_decay_rejects_nan() {
208 let err = base_note().try_with_decay(f64::NAN).unwrap_err();
209 assert!(err.contains("finite"), "error must mention finite: {err}");
210 }
211
212 #[test]
213 fn try_with_decay_rejects_negative() {
214 let err = base_note().try_with_decay(-0.1).unwrap_err();
215 assert!(err.contains("0.0"), "error must mention bound: {err}");
216 }
217}
218
219#[derive(Clone, Debug, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222pub enum SortDir {
223 Asc,
224 Desc,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum FilterOp {
231 Eq,
232 EqOrMissing,
235 Ne,
236 Lt,
237 Lte,
238 Gt,
239 Gte,
240 JsonTypeEq,
244 JsonTypeNeMissing,
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize)]
255pub struct PropertyFilter {
256 pub json_path: String,
257 pub op: FilterOp,
258 pub value: SqlValue,
259}
260
261#[derive(Clone, Debug, Default, Serialize, Deserialize)]
266pub struct NoteFilter {
267 pub kind: Option<String>,
268 #[serde(default)]
269 pub property_filters: Vec<PropertyFilter>,
270 pub order_by: Option<(String, SortDir)>,
272}
273
274#[async_trait]
276pub trait NoteStore: Send + Sync + 'static {
277 async fn upsert_note(&self, note: Note) -> StorageResult<()>;
279 async fn upsert_notes(&self, notes: Vec<Note>) -> StorageResult<BatchWriteSummary>;
281 async fn get_note(&self, id: Uuid) -> StorageResult<Option<Note>>;
283 async fn get_note_including_deleted(&self, id: Uuid) -> StorageResult<Option<Note>>;
288 async fn delete_note(&self, id: Uuid, mode: DeleteMode) -> StorageResult<bool>;
290 async fn query_notes(
292 &self,
293 namespace: &str,
294 kind: Option<&str>,
295 page: PageRequest,
296 ) -> StorageResult<Page<Note>>;
297 async fn query_notes_filtered(
299 &self,
300 namespace: &str,
301 filter: &NoteFilter,
302 page: PageRequest,
303 ) -> StorageResult<Page<Note>>;
304 async fn count_notes(&self, namespace: &str, kind: Option<&str>) -> StorageResult<u64>;
306
307 async fn get_notes_batch(&self, ids: &[Uuid]) -> StorageResult<Vec<Note>> {
309 let mut out = Vec::with_capacity(ids.len());
310 for &id in ids {
311 if let Some(n) = self.get_note(id).await? {
312 out.push(n);
313 }
314 }
315 Ok(out)
316 }
317}