1use super::ContextTemplateStorage;
7use crate::models::{ContextTemplate, OutputFormat, TemplateVariable};
8use crate::{Error, Result};
9use rusqlite::{Connection, OptionalExtension, params};
10use std::path::{Path, PathBuf};
11use std::sync::Mutex;
12
13pub struct SqliteContextTemplateStorage {
15 conn: Mutex<Connection>,
17 db_path: PathBuf,
19}
20
21impl SqliteContextTemplateStorage {
22 pub fn new(db_path: impl Into<PathBuf>) -> Result<Self> {
32 let db_path = db_path.into();
33
34 if let Some(parent) = db_path.parent() {
36 std::fs::create_dir_all(parent).map_err(|e| Error::OperationFailed {
37 operation: "create_context_template_dir".to_string(),
38 cause: e.to_string(),
39 })?;
40 }
41
42 let conn = Connection::open(&db_path).map_err(|e| Error::OperationFailed {
43 operation: "open_context_template_db".to_string(),
44 cause: e.to_string(),
45 })?;
46
47 let storage = Self {
48 conn: Mutex::new(conn),
49 db_path,
50 };
51
52 storage.initialize()?;
53 Ok(storage)
54 }
55
56 pub fn in_memory() -> Result<Self> {
62 let conn = Connection::open_in_memory().map_err(|e| Error::OperationFailed {
63 operation: "open_context_template_db_memory".to_string(),
64 cause: e.to_string(),
65 })?;
66
67 let storage = Self {
68 conn: Mutex::new(conn),
69 db_path: PathBuf::from(":memory:"),
70 };
71
72 storage.initialize()?;
73 Ok(storage)
74 }
75
76 #[must_use]
80 pub fn default_user_path() -> Option<PathBuf> {
81 directories::BaseDirs::new().map(|d| {
82 d.home_dir()
83 .join(".config")
84 .join("subcog")
85 .join("memories.db")
86 })
87 }
88
89 #[must_use]
93 pub fn default_org_path(org: &str) -> Option<PathBuf> {
94 directories::BaseDirs::new().map(|d| {
95 d.home_dir()
96 .join(".config")
97 .join("subcog")
98 .join("orgs")
99 .join(org)
100 .join("memories.db")
101 })
102 }
103
104 #[must_use]
106 pub fn db_path(&self) -> &Path {
107 &self.db_path
108 }
109
110 fn initialize(&self) -> Result<()> {
112 let conn = self.conn.lock().map_err(|e| Error::OperationFailed {
113 operation: "lock_context_template_db".to_string(),
114 cause: e.to_string(),
115 })?;
116
117 let _ = conn.pragma_update(None, "journal_mode", "WAL");
119 let _ = conn.pragma_update(None, "synchronous", "NORMAL");
120 let _ = conn.pragma_update(None, "busy_timeout", "5000");
121
122 conn.execute(
123 "CREATE TABLE IF NOT EXISTS context_templates (
124 name TEXT NOT NULL,
125 version INTEGER NOT NULL,
126 description TEXT NOT NULL DEFAULT '',
127 content TEXT NOT NULL,
128 variables TEXT NOT NULL DEFAULT '[]',
129 tags TEXT NOT NULL DEFAULT '[]',
130 output_format TEXT NOT NULL DEFAULT 'markdown',
131 author TEXT,
132 created_at INTEGER NOT NULL,
133 updated_at INTEGER NOT NULL,
134 PRIMARY KEY (name, version)
135 )",
136 [],
137 )
138 .map_err(|e| Error::OperationFailed {
139 operation: "create_context_templates_table".to_string(),
140 cause: e.to_string(),
141 })?;
142
143 conn.execute(
145 "CREATE INDEX IF NOT EXISTS idx_context_templates_name_version
146 ON context_templates(name, version DESC)",
147 [],
148 )
149 .map_err(|e| Error::OperationFailed {
150 operation: "create_context_templates_index".to_string(),
151 cause: e.to_string(),
152 })?;
153
154 conn.execute(
156 "CREATE INDEX IF NOT EXISTS idx_context_templates_tags
157 ON context_templates(tags)",
158 [],
159 )
160 .map_err(|e| Error::OperationFailed {
161 operation: "create_context_templates_tags_index".to_string(),
162 cause: e.to_string(),
163 })?;
164
165 Ok(())
166 }
167
168 fn lock_conn(&self) -> Result<std::sync::MutexGuard<'_, Connection>> {
170 self.conn.lock().map_err(|e| Error::OperationFailed {
171 operation: "lock_context_template_db".to_string(),
172 cause: e.to_string(),
173 })
174 }
175
176 pub fn vacuum_and_analyze(&self) -> Result<()> {
182 let conn = self.lock_conn()?;
183
184 conn.execute("VACUUM", [])
185 .map_err(|e| Error::OperationFailed {
186 operation: "context_template_db_vacuum".to_string(),
187 cause: e.to_string(),
188 })?;
189
190 conn.execute("ANALYZE", [])
191 .map_err(|e| Error::OperationFailed {
192 operation: "context_template_db_analyze".to_string(),
193 cause: e.to_string(),
194 })?;
195
196 Ok(())
197 }
198
199 #[must_use]
201 pub fn stats(&self) -> Option<ContextTemplateDbStats> {
202 let conn = self.lock_conn().ok()?;
203
204 let template_count: i64 = conn
205 .query_row(
206 "SELECT COUNT(DISTINCT name) FROM context_templates",
207 [],
208 |row| row.get(0),
209 )
210 .unwrap_or(0);
211
212 let version_count: i64 = conn
213 .query_row("SELECT COUNT(*) FROM context_templates", [], |row| {
214 row.get(0)
215 })
216 .unwrap_or(0);
217
218 let page_count: i64 = conn
219 .pragma_query_value(None, "page_count", |row| row.get(0))
220 .unwrap_or(0);
221
222 let page_size: i64 = conn
223 .pragma_query_value(None, "page_size", |row| row.get(0))
224 .unwrap_or(4096);
225
226 Some(ContextTemplateDbStats {
227 template_count: u64::try_from(template_count).unwrap_or(0),
228 version_count: u64::try_from(version_count).unwrap_or(0),
229 db_size_bytes: u64::try_from(page_count.saturating_mul(page_size)).unwrap_or(0),
230 })
231 }
232
233 fn parse_output_format(s: &str) -> OutputFormat {
235 match s.to_lowercase().as_str() {
236 "json" => OutputFormat::Json,
237 "xml" => OutputFormat::Xml,
238 _ => OutputFormat::Markdown,
239 }
240 }
241
242 const fn format_to_string(format: OutputFormat) -> &'static str {
244 match format {
245 OutputFormat::Markdown => "markdown",
246 OutputFormat::Json => "json",
247 OutputFormat::Xml => "xml",
248 }
249 }
250}
251
252#[derive(Debug, Clone, Copy, Default)]
254pub struct ContextTemplateDbStats {
255 pub template_count: u64,
257 pub version_count: u64,
259 pub db_size_bytes: u64,
261}
262
263impl ContextTemplateStorage for SqliteContextTemplateStorage {
264 #[allow(clippy::cast_possible_wrap)]
265 fn save(&self, template: &ContextTemplate) -> Result<(String, u32)> {
266 let conn = self.lock_conn()?;
267
268 let next_version: u32 = conn
270 .query_row(
271 "SELECT COALESCE(MAX(version), 0) + 1 FROM context_templates WHERE name = ?1",
272 params![template.name],
273 |row| row.get::<_, u32>(0),
274 )
275 .map_err(|e| Error::OperationFailed {
276 operation: "get_next_version".to_string(),
277 cause: e.to_string(),
278 })?;
279
280 let variables_json =
281 serde_json::to_string(&template.variables).map_err(|e| Error::OperationFailed {
282 operation: "serialize_variables".to_string(),
283 cause: e.to_string(),
284 })?;
285
286 let tags_json =
287 serde_json::to_string(&template.tags).map_err(|e| Error::OperationFailed {
288 operation: "serialize_tags".to_string(),
289 cause: e.to_string(),
290 })?;
291
292 let now = crate::current_timestamp();
293 let output_format = Self::format_to_string(template.output_format);
294
295 conn.execute(
296 "INSERT INTO context_templates
297 (name, version, description, content, variables, tags, output_format, author, created_at, updated_at)
298 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9)",
299 params![
300 template.name,
301 next_version,
302 template.description,
303 template.content,
304 variables_json,
305 tags_json,
306 output_format,
307 template.author,
308 now as i64,
309 ],
310 )
311 .map_err(|e| Error::OperationFailed {
312 operation: "save_context_template".to_string(),
313 cause: e.to_string(),
314 })?;
315
316 Ok((template.name.clone(), next_version))
317 }
318
319 #[allow(clippy::cast_sign_loss, clippy::option_if_let_else)]
320 fn get(&self, name: &str, version: Option<u32>) -> Result<Option<ContextTemplate>> {
321 let conn = self.lock_conn()?;
322
323 let sql = if version.is_some() {
324 "SELECT name, version, description, content, variables, tags, output_format, author, created_at, updated_at
325 FROM context_templates WHERE name = ?1 AND version = ?2"
326 } else {
327 "SELECT name, version, description, content, variables, tags, output_format, author, created_at, updated_at
328 FROM context_templates WHERE name = ?1 ORDER BY version DESC LIMIT 1"
329 };
330
331 let result = if let Some(v) = version {
332 conn.query_row(sql, params![name, v], |row| {
333 Ok((
334 row.get::<_, String>(0)?,
335 row.get::<_, u32>(1)?,
336 row.get::<_, String>(2)?,
337 row.get::<_, String>(3)?,
338 row.get::<_, String>(4)?,
339 row.get::<_, String>(5)?,
340 row.get::<_, String>(6)?,
341 row.get::<_, Option<String>>(7)?,
342 row.get::<_, i64>(8)?,
343 row.get::<_, i64>(9)?,
344 ))
345 })
346 } else {
347 conn.query_row(sql, params![name], |row| {
348 Ok((
349 row.get::<_, String>(0)?,
350 row.get::<_, u32>(1)?,
351 row.get::<_, String>(2)?,
352 row.get::<_, String>(3)?,
353 row.get::<_, String>(4)?,
354 row.get::<_, String>(5)?,
355 row.get::<_, String>(6)?,
356 row.get::<_, Option<String>>(7)?,
357 row.get::<_, i64>(8)?,
358 row.get::<_, i64>(9)?,
359 ))
360 })
361 }
362 .optional()
363 .map_err(|e| Error::OperationFailed {
364 operation: "get_context_template".to_string(),
365 cause: e.to_string(),
366 })?;
367
368 match result {
369 Some((
370 name,
371 version,
372 description,
373 content,
374 variables_json,
375 tags_json,
376 output_format,
377 author,
378 created_at,
379 updated_at,
380 )) => {
381 let variables: Vec<TemplateVariable> =
382 serde_json::from_str(&variables_json).unwrap_or_default();
383 let tags: Vec<String> = serde_json::from_str(&tags_json).unwrap_or_default();
384
385 Ok(Some(ContextTemplate {
386 name,
387 version,
388 description,
389 content,
390 variables,
391 tags,
392 output_format: Self::parse_output_format(&output_format),
393 author,
394 created_at: created_at as u64,
395 updated_at: updated_at as u64,
396 }))
397 },
398 None => Ok(None),
399 }
400 }
401
402 #[allow(clippy::cast_sign_loss)]
403 fn list(
404 &self,
405 tags: Option<&[String]>,
406 name_pattern: Option<&str>,
407 ) -> Result<Vec<ContextTemplate>> {
408 let conn = self.lock_conn()?;
409
410 let mut sql = String::from(
412 "SELECT ct.name, ct.version, ct.description, ct.content, ct.variables, ct.tags,
413 ct.output_format, ct.author, ct.created_at, ct.updated_at
414 FROM context_templates ct
415 INNER JOIN (
416 SELECT name, MAX(version) as max_version
417 FROM context_templates
418 GROUP BY name
419 ) latest ON ct.name = latest.name AND ct.version = latest.max_version
420 WHERE 1=1",
421 );
422 let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
423
424 if let Some(pattern) = name_pattern {
426 let like_pattern = pattern.replace('*', "%").replace('?', "_");
427 sql.push_str(" AND ct.name LIKE ?");
428 params_vec.push(Box::new(like_pattern));
429 }
430
431 sql.push_str(" ORDER BY ct.name ASC");
432
433 let mut stmt = conn.prepare(&sql).map_err(|e| Error::OperationFailed {
434 operation: "prepare_list_context_templates".to_string(),
435 cause: e.to_string(),
436 })?;
437
438 let params_refs: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(AsRef::as_ref).collect();
439
440 let rows = stmt
441 .query_map(params_refs.as_slice(), |row| {
442 Ok((
443 row.get::<_, String>(0)?,
444 row.get::<_, u32>(1)?,
445 row.get::<_, String>(2)?,
446 row.get::<_, String>(3)?,
447 row.get::<_, String>(4)?,
448 row.get::<_, String>(5)?,
449 row.get::<_, String>(6)?,
450 row.get::<_, Option<String>>(7)?,
451 row.get::<_, i64>(8)?,
452 row.get::<_, i64>(9)?,
453 ))
454 })
455 .map_err(|e| Error::OperationFailed {
456 operation: "list_context_templates".to_string(),
457 cause: e.to_string(),
458 })?;
459
460 let mut results = Vec::new();
461 for row in rows {
462 let (
463 name,
464 version,
465 description,
466 content,
467 variables_json,
468 tags_json,
469 output_format,
470 author,
471 created_at,
472 updated_at,
473 ) = row.map_err(|e| Error::OperationFailed {
474 operation: "read_context_template_row".to_string(),
475 cause: e.to_string(),
476 })?;
477
478 let variables: Vec<TemplateVariable> =
479 serde_json::from_str(&variables_json).unwrap_or_default();
480 let template_tags: Vec<String> = serde_json::from_str(&tags_json).unwrap_or_default();
481
482 let has_all_required_tags = tags.is_none_or(|required_tags| {
484 required_tags.iter().all(|t| template_tags.contains(t))
485 });
486 if !has_all_required_tags {
487 continue;
488 }
489
490 results.push(ContextTemplate {
491 name,
492 version,
493 description,
494 content,
495 variables,
496 tags: template_tags,
497 output_format: Self::parse_output_format(&output_format),
498 author,
499 created_at: created_at as u64,
500 updated_at: updated_at as u64,
501 });
502 }
503
504 Ok(results)
505 }
506
507 #[allow(clippy::option_if_let_else)]
508 fn delete(&self, name: &str, version: Option<u32>) -> Result<bool> {
509 let conn = self.lock_conn()?;
510
511 let rows_affected = if let Some(v) = version {
512 conn.execute(
513 "DELETE FROM context_templates WHERE name = ?1 AND version = ?2",
514 params![name, v],
515 )
516 } else {
517 conn.execute(
518 "DELETE FROM context_templates WHERE name = ?1",
519 params![name],
520 )
521 }
522 .map_err(|e| Error::OperationFailed {
523 operation: "delete_context_template".to_string(),
524 cause: e.to_string(),
525 })?;
526
527 Ok(rows_affected > 0)
528 }
529
530 fn get_versions(&self, name: &str) -> Result<Vec<u32>> {
531 let conn = self.lock_conn()?;
532
533 let mut stmt = conn
534 .prepare("SELECT version FROM context_templates WHERE name = ?1 ORDER BY version DESC")
535 .map_err(|e| Error::OperationFailed {
536 operation: "prepare_get_versions".to_string(),
537 cause: e.to_string(),
538 })?;
539
540 let versions = stmt
541 .query_map(params![name], |row| row.get::<_, u32>(0))
542 .map_err(|e| Error::OperationFailed {
543 operation: "get_versions".to_string(),
544 cause: e.to_string(),
545 })?
546 .collect::<std::result::Result<Vec<_>, _>>()
547 .map_err(|e| Error::OperationFailed {
548 operation: "collect_versions".to_string(),
549 cause: e.to_string(),
550 })?;
551
552 Ok(versions)
553 }
554
555 fn get_latest_version(&self, name: &str) -> Result<Option<u32>> {
556 let conn = self.lock_conn()?;
557
558 conn.query_row(
559 "SELECT MAX(version) FROM context_templates WHERE name = ?1",
560 params![name],
561 |row| row.get::<_, Option<u32>>(0),
562 )
563 .map_err(|e| Error::OperationFailed {
564 operation: "get_latest_version".to_string(),
565 cause: e.to_string(),
566 })
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use crate::models::VariableType;
574
575 fn create_test_template(name: &str, content: &str) -> ContextTemplate {
576 ContextTemplate {
577 name: name.to_string(),
578 version: 0, description: format!("Test template: {name}"),
580 content: content.to_string(),
581 variables: vec![],
582 tags: vec![],
583 output_format: OutputFormat::Markdown,
584 author: None,
585 created_at: 0,
586 updated_at: 0,
587 }
588 }
589
590 #[test]
591 fn test_sqlite_context_template_storage_creation() {
592 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
593 assert_eq!(storage.db_path().to_str(), Some(":memory:"));
594 }
595
596 #[test]
597 fn test_save_and_get_template() {
598 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
599
600 let template = create_test_template("test-template", "Hello {{name}}!");
601
602 let (name, version) = storage.save(&template).unwrap();
603 assert_eq!(name, "test-template");
604 assert_eq!(version, 1);
605
606 let retrieved = storage.get("test-template", None).unwrap();
607 assert!(retrieved.is_some());
608 let retrieved = retrieved.unwrap();
609 assert_eq!(retrieved.name, "test-template");
610 assert_eq!(retrieved.version, 1);
611 assert_eq!(retrieved.content, "Hello {{name}}!");
612 }
613
614 #[test]
615 fn test_auto_increment_version() {
616 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
617
618 let template = create_test_template("versioned", "Version 1");
620 let (_, v1) = storage.save(&template).unwrap();
621 assert_eq!(v1, 1);
622
623 let template2 = create_test_template("versioned", "Version 2");
625 let (_, v2) = storage.save(&template2).unwrap();
626 assert_eq!(v2, 2);
627
628 let template3 = create_test_template("versioned", "Version 3");
630 let (_, v3) = storage.save(&template3).unwrap();
631 assert_eq!(v3, 3);
632
633 let latest = storage.get("versioned", None).unwrap().unwrap();
635 assert_eq!(latest.version, 3);
636 assert_eq!(latest.content, "Version 3");
637
638 let v1_retrieved = storage.get("versioned", Some(1)).unwrap().unwrap();
640 assert_eq!(v1_retrieved.version, 1);
641 assert_eq!(v1_retrieved.content, "Version 1");
642 }
643
644 #[test]
645 fn test_get_versions() {
646 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
647
648 for i in 1..=5 {
650 let template = create_test_template("multi-version", &format!("Content {i}"));
651 storage.save(&template).unwrap();
652 }
653
654 let versions = storage.get_versions("multi-version").unwrap();
655 assert_eq!(versions, vec![5, 4, 3, 2, 1]);
656 }
657
658 #[test]
659 fn test_get_latest_version() {
660 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
661
662 assert_eq!(storage.get_latest_version("nonexistent").unwrap(), None);
664
665 for _ in 0..3 {
667 let template = create_test_template("latest-test", "Content");
668 storage.save(&template).unwrap();
669 }
670
671 assert_eq!(storage.get_latest_version("latest-test").unwrap(), Some(3));
672 }
673
674 #[test]
675 fn test_list_templates() {
676 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
677
678 for i in 1..=3 {
680 let template = create_test_template("alpha", &format!("Alpha v{i}"));
681 storage.save(&template).unwrap();
682 }
683
684 let mut beta = create_test_template("beta", "Beta content");
685 beta.tags = vec!["tag1".to_string(), "tag2".to_string()];
686 storage.save(&beta).unwrap();
687
688 let gamma = create_test_template("gamma", "Gamma content");
689 storage.save(&gamma).unwrap();
690
691 let all = storage.list(None, None).unwrap();
693 assert_eq!(all.len(), 3);
694
695 let alpha = all.iter().find(|t| t.name == "alpha").unwrap();
697 assert_eq!(alpha.version, 3);
698
699 let with_tag1 = storage.list(Some(&["tag1".to_string()]), None).unwrap();
701 assert_eq!(with_tag1.len(), 1);
702 assert_eq!(with_tag1[0].name, "beta");
703
704 let alpha_pattern = storage.list(None, Some("a*")).unwrap();
706 assert_eq!(alpha_pattern.len(), 1);
707 assert_eq!(alpha_pattern[0].name, "alpha");
708 }
709
710 #[test]
711 fn test_delete_specific_version() {
712 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
713
714 for _ in 0..3 {
716 let template = create_test_template("to-delete", "Content");
717 storage.save(&template).unwrap();
718 }
719
720 assert!(storage.delete("to-delete", Some(2)).unwrap());
722
723 assert!(storage.get("to-delete", Some(2)).unwrap().is_none());
725
726 assert!(storage.get("to-delete", Some(1)).unwrap().is_some());
728 assert!(storage.get("to-delete", Some(3)).unwrap().is_some());
729
730 let latest = storage.get("to-delete", None).unwrap().unwrap();
732 assert_eq!(latest.version, 3);
733 }
734
735 #[test]
736 fn test_delete_all_versions() {
737 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
738
739 for _ in 0..3 {
741 let template = create_test_template("delete-all", "Content");
742 storage.save(&template).unwrap();
743 }
744
745 assert!(storage.delete("delete-all", None).unwrap());
747
748 assert!(storage.get("delete-all", None).unwrap().is_none());
750 assert!(storage.get_versions("delete-all").unwrap().is_empty());
751 }
752
753 #[test]
754 fn test_template_with_variables() {
755 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
756
757 let template = ContextTemplate {
758 name: "with-vars".to_string(),
759 version: 0,
760 description: "Template with variables".to_string(),
761 content: "Hello {{name}}, you have {{count}} items".to_string(),
762 variables: vec![
763 TemplateVariable {
764 name: "name".to_string(),
765 var_type: VariableType::User,
766 description: Some("User's name".to_string()),
767 default: None,
768 required: true,
769 },
770 TemplateVariable {
771 name: "count".to_string(),
772 var_type: VariableType::User,
773 description: None,
774 default: Some("0".to_string()),
775 required: false,
776 },
777 ],
778 tags: vec!["greeting".to_string()],
779 output_format: OutputFormat::Markdown,
780 author: Some("test-author".to_string()),
781 created_at: 0,
782 updated_at: 0,
783 };
784
785 storage.save(&template).unwrap();
786
787 let retrieved = storage.get("with-vars", None).unwrap().unwrap();
788 assert_eq!(retrieved.variables.len(), 2);
789 assert_eq!(retrieved.variables[0].name, "name");
790 assert!(retrieved.variables[0].required);
791 assert_eq!(retrieved.author, Some("test-author".to_string()));
792 }
793
794 #[test]
795 fn test_output_format_persistence() {
796 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
797
798 let mut template = create_test_template("json-format", "{{data}}");
800 template.output_format = OutputFormat::Json;
801 storage.save(&template).unwrap();
802
803 let retrieved = storage.get("json-format", None).unwrap().unwrap();
804 assert!(matches!(retrieved.output_format, OutputFormat::Json));
805
806 let mut template2 = create_test_template("xml-format", "{{data}}");
808 template2.output_format = OutputFormat::Xml;
809 storage.save(&template2).unwrap();
810
811 let retrieved2 = storage.get("xml-format", None).unwrap().unwrap();
812 assert!(matches!(retrieved2.output_format, OutputFormat::Xml));
813 }
814
815 #[test]
816 fn test_stats() {
817 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
818
819 let stats = storage.stats().unwrap();
821 assert_eq!(stats.template_count, 0);
822 assert_eq!(stats.version_count, 0);
823
824 for _ in 0..3 {
826 let template = create_test_template("stats-test-1", "Content");
827 storage.save(&template).unwrap();
828 }
829 storage
830 .save(&create_test_template("stats-test-2", "Content"))
831 .unwrap();
832
833 let stats = storage.stats().unwrap();
834 assert_eq!(stats.template_count, 2); assert_eq!(stats.version_count, 4); assert!(stats.db_size_bytes > 0);
837 }
838
839 #[test]
840 fn test_vacuum_and_analyze() {
841 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
842
843 for i in 0..10 {
845 storage
846 .save(&create_test_template(&format!("temp-{i}"), "Content"))
847 .unwrap();
848 }
849 for i in 0..10 {
850 storage.delete(&format!("temp-{i}"), None).unwrap();
851 }
852
853 assert!(storage.vacuum_and_analyze().is_ok());
854 }
855
856 #[test]
857 fn test_default_user_path() {
858 let path = SqliteContextTemplateStorage::default_user_path();
859 if let Some(p) = path {
860 assert!(p.to_string_lossy().contains("subcog"));
861 assert!(p.to_string_lossy().ends_with("memories.db"));
862 }
863 }
864
865 #[test]
866 fn test_nonexistent_template() {
867 let storage = SqliteContextTemplateStorage::in_memory().unwrap();
868
869 assert!(storage.get("nonexistent", None).unwrap().is_none());
870 assert!(storage.get("nonexistent", Some(1)).unwrap().is_none());
871 assert!(!storage.delete("nonexistent", None).unwrap());
872 }
873}