1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! Database update operations.
use chrono::Utc;
use super::{Error, Result, vec_to_blob};
/// Options for updating a memory in the database.
///
/// Groups the optional fields that may be updated, reducing the `Database::update`
/// function signature and making call sites self-documenting.
pub struct UpdateOptions<'a> {
/// New content for the memory.
pub content: Option<&'a str>,
/// Pre-computed embedding (required when content is provided).
pub embedding: Option<&'a [f32]>,
/// New metadata JSON string (full replacement).
pub metadata: Option<&'a str>,
/// New memory type.
pub memory_type: Option<&'a str>,
/// New lifecycle status.
pub status: Option<&'a str>,
}
impl super::Database {
/// Update a memory's content and/or metadata.
///
/// - If content is provided: updates content and embedding
/// - If metadata is provided: updates metadata (full replacement, not merge)
/// - If both provided: updates both
///
/// Returns an error if the memory does not exist.
///
/// # Errors
///
/// Returns error if:
/// - Embedding has invalid dimensions (when content is provided)
/// - Memory not found
/// - Query fails
pub fn update(&self, id: &str, project_id: &str, options: UpdateOptions<'_>) -> Result<()> {
let now = Utc::now().to_rfc3339();
// Build dynamic UPDATE query based on what's being updated
let mut set_clauses: Vec<&str> = Vec::new();
let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(text) = options.content {
set_clauses.push("content = ?");
let blob =
vec_to_blob(options.embedding.ok_or_else(|| {
Error::Sqlite("Content update requires embedding".to_string())
})?)?;
params.push(Box::new(text.to_string()));
set_clauses.push("embedding = ?");
params.push(Box::new(blob));
}
if let Some(meta) = options.metadata {
set_clauses.push("metadata = ?");
params.push(Box::new(meta.to_string()));
}
if let Some(t) = options.memory_type {
set_clauses.push("type = ?");
params.push(Box::new(t.to_string()));
}
if let Some(s) = options.status {
set_clauses.push("status = ?");
params.push(Box::new(s.to_string()));
}
set_clauses.push("updated_at = ?");
params.push(Box::new(now));
// Guard: set_clauses.len() == 1 means only `updated_at` was pushed (it is always
// appended unconditionally). That happens when NO optional fields were supplied,
// i.e. the caller asked to update nothing. We reject this rather than running a
// no-op UPDATE that silently returns 0 rows affected.
if set_clauses.len() == 1 {
return Err(Error::InvalidInput(
"At least one field must be provided for update".to_string(),
));
}
let sql = format!(
"UPDATE memories SET {} WHERE id = ? AND project_id = ?",
set_clauses.join(", ")
);
// Add id and project_id as last parameters
params.push(Box::new(id.to_string()));
params.push(Box::new(project_id.to_string()));
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
let rows = self.conn.execute(&sql, param_refs.as_slice())?;
if rows == 0 {
return Err(Error::NotFound(
"No memory found for the given id".to_string(),
));
}
Ok(())
}
}