1use super::Database;
4use crate::db::metadata::{MetadataFilter, UserMetadata};
5use crate::error::Result;
6use rusqlite::params;
7
8impl Database {
9 pub fn add_metadata(&self, docid: &str, metadata: &UserMetadata) -> Result<()> {
28 let docid = docid.trim_start_matches('#');
29
30 let doc_id = self.conn.query_row(
32 "SELECT d.id FROM documents d
33 JOIN content c ON c.hash = d.hash
34 WHERE substr(c.hash, 1, 6) = ?1 AND d.active = 1
35 LIMIT 1",
36 params![docid],
37 |row| row.get::<_, i64>(0),
38 )?;
39
40 let existing_json: Option<String> = self
42 .conn
43 .query_row(
44 "SELECT user_metadata FROM documents WHERE id = ?1",
45 params![doc_id],
46 |row| row.get(0),
47 )
48 .ok()
49 .flatten();
50
51 let mut combined = if let Some(json) = existing_json {
53 UserMetadata::from_json(&json).unwrap_or_default()
54 } else {
55 UserMetadata::new()
56 };
57
58 combined.merge(metadata);
59
60 let json = combined.to_json()?;
62 self.conn.execute(
63 "UPDATE documents SET user_metadata = ?1 WHERE id = ?2",
64 params![json, doc_id],
65 )?;
66
67 Ok(())
68 }
69
70 pub fn get_metadata(&self, docid: &str) -> Result<Option<UserMetadata>> {
72 let docid = docid.trim_start_matches('#');
73
74 let result: Option<String> = self
75 .conn
76 .query_row(
77 "SELECT d.user_metadata FROM documents d
78 JOIN content c ON c.hash = d.hash
79 WHERE substr(c.hash, 1, 6) = ?1 AND d.active = 1
80 LIMIT 1",
81 params![docid],
82 |row| row.get(0),
83 )
84 .ok()
85 .flatten();
86
87 match result {
88 Some(json) => Ok(Some(UserMetadata::from_json(&json)?)),
89 None => Ok(None),
90 }
91 }
92
93 pub fn remove_metadata_fields(&self, docid: &str, fields: &[String]) -> Result<()> {
95 if let Some(mut metadata) = self.get_metadata(docid)? {
96 for field in fields {
97 metadata.remove(field);
98 }
99
100 let docid_clean = docid.trim_start_matches('#');
101 let doc_id: i64 = self.conn.query_row(
102 "SELECT d.id FROM documents d
103 JOIN content c ON c.hash = d.hash
104 WHERE substr(c.hash, 1, 6) = ?1 AND d.active = 1
105 LIMIT 1",
106 params![docid_clean],
107 |row| row.get(0),
108 )?;
109
110 let json = metadata.to_json()?;
111 self.conn.execute(
112 "UPDATE documents SET user_metadata = ?1 WHERE id = ?2",
113 params![json, doc_id],
114 )?;
115 }
116
117 Ok(())
118 }
119
120 pub fn clear_metadata(&self, docid: &str) -> Result<()> {
122 let docid = docid.trim_start_matches('#');
123
124 self.conn.execute(
125 "UPDATE documents d
126 SET user_metadata = NULL
127 WHERE d.id IN (
128 SELECT d2.id FROM documents d2
129 JOIN content c ON c.hash = d2.hash
130 WHERE substr(c.hash, 1, 6) = ?1 AND d2.active = 1
131 LIMIT 1
132 )",
133 params![docid],
134 )?;
135
136 Ok(())
137 }
138
139 pub fn find_by_metadata(&self, filter: &MetadataFilter, limit: usize) -> Result<Vec<String>> {
141 let mut stmt = self.conn.prepare(
142 "SELECT d.id, c.hash, d.user_metadata
143 FROM documents d
144 JOIN content c ON c.hash = d.hash
145 WHERE d.active = 1 AND d.user_metadata IS NOT NULL
146 LIMIT ?1",
147 )?;
148
149 let docids: Vec<String> = stmt
150 .query_map(params![limit], |row| {
151 let hash: String = row.get(1)?;
152 let metadata_json: Option<String> = row.get(2)?;
153
154 if let Some(json) = metadata_json {
155 if let Ok(metadata) = UserMetadata::from_json(&json) {
156 if filter.matches(&metadata) {
157 return Ok(Some(format!("#{}", &hash[..6])));
158 }
159 }
160 }
161 Ok(None)
162 })?
163 .filter_map(|r| r.ok().flatten())
164 .collect();
165
166 Ok(docids)
167 }
168
169 pub fn list_with_metadata(&self, limit: usize) -> Result<Vec<(String, UserMetadata)>> {
171 let mut stmt = self.conn.prepare(
172 "SELECT c.hash, d.user_metadata
173 FROM documents d
174 JOIN content c ON c.hash = d.hash
175 WHERE d.active = 1 AND d.user_metadata IS NOT NULL
176 LIMIT ?1",
177 )?;
178
179 let results: Vec<(String, UserMetadata)> = stmt
180 .query_map(params![limit], |row| {
181 let hash: String = row.get(0)?;
182 let metadata_json: String = row.get(1)?;
183 Ok((hash, metadata_json))
184 })?
185 .filter_map(|r| r.ok())
186 .filter_map(|(hash, json)| {
187 UserMetadata::from_json(&json)
188 .ok()
189 .map(|m| (format!("#{}", &hash[..6]), m))
190 })
191 .collect();
192
193 Ok(results)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::db::metadata::{MetadataBuilder, MetadataFilter};
201 use chrono::Utc;
202
203 #[test]
204 fn test_add_and_get_metadata() {
205 let db = Database::open_in_memory().unwrap();
206 db.initialize().unwrap();
207
208 let now = Utc::now().to_rfc3339();
210 db.insert_content("testhash123", "test content").unwrap();
211 db.insert_document(
212 "test",
213 "test.md",
214 "Test",
215 "testhash123",
216 &now,
217 &now,
218 "file",
219 None,
220 )
221 .unwrap();
222
223 let metadata = MetadataBuilder::new()
225 .text("author", "Alice")
226 .tags("labels", vec!["test", "example"])
227 .integer("version", 1)
228 .build();
229
230 let docid = format!("#{}", &"testhash123"[..6]);
232
233 db.add_metadata(&docid, &metadata).unwrap();
234
235 let retrieved = db.get_metadata(&docid).unwrap();
237 assert!(retrieved.is_some());
238
239 let retrieved = retrieved.unwrap();
240 assert!(retrieved.contains("author"));
241 assert!(retrieved.contains("labels"));
242 assert!(retrieved.contains("version"));
243 }
244
245 #[test]
246 fn test_metadata_merge() {
247 let db = Database::open_in_memory().unwrap();
248 db.initialize().unwrap();
249
250 let now = Utc::now().to_rfc3339();
251 db.insert_content("testhash456", "test content").unwrap();
252 db.insert_document(
253 "test",
254 "test.md",
255 "Test",
256 "testhash456",
257 &now,
258 &now,
259 "file",
260 None,
261 )
262 .unwrap();
263
264 let docid = format!("#{}", &"testhash456"[..6]);
265
266 let meta1 = MetadataBuilder::new().text("author", "Alice").build();
268 db.add_metadata(&docid, &meta1).unwrap();
269
270 let meta2 = MetadataBuilder::new().tags("labels", vec!["rust"]).build();
272 db.add_metadata(&docid, &meta2).unwrap();
273
274 let retrieved = db.get_metadata(&docid).unwrap().unwrap();
276 assert!(retrieved.contains("author"));
277 assert!(retrieved.contains("labels"));
278 }
279
280 #[test]
281 fn test_find_by_metadata() {
282 let db = Database::open_in_memory().unwrap();
283 db.initialize().unwrap();
284
285 let now = Utc::now().to_rfc3339();
286
287 for i in 1..=3 {
289 let hash = format!("hash{}_abcdef", i);
290 let content = format!("content {}", i);
291 db.insert_content(&hash, &content).unwrap();
292 db.insert_document(
293 "test",
294 &format!("doc{}.md", i),
295 "Test",
296 &hash,
297 &now,
298 &now,
299 "file",
300 None,
301 )
302 .unwrap();
303
304 let metadata = MetadataBuilder::new().integer("score", i as i64).build();
305
306 let docid = format!("#{}", &hash[..6]);
307 db.add_metadata(&docid, &metadata).unwrap();
308 }
309
310 let filter = MetadataFilter::IntegerGt("score".to_string(), 1);
312 let results = db.find_by_metadata(&filter, 10).unwrap();
313
314 assert_eq!(results.len(), 2); }
316}