use super::{Database, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportRow {
pub id: String,
pub project_id: String,
pub content: String,
pub metadata: Option<String>,
pub embedding_blob: Vec<u8>,
pub created_at: String,
pub updated_at: String,
pub memory_type: String,
pub status: String,
pub superseded_by: Option<String>,
pub retrieval_count: i64,
pub last_retrieved_at: Option<String>,
}
impl Database {
pub fn scan_all_rows(&self) -> Result<Vec<ExportRow>> {
let mut stmt = self.conn.prepare(
r#"
SELECT id, project_id, content, metadata, embedding, created_at, updated_at, type, status, superseded_by, retrieval_count, last_retrieved_at, importance
FROM memories
ORDER BY rowid
"#,
)?;
let rows = stmt.query_map([], |row| {
Ok(ExportRow {
id: row.get(0)?,
project_id: row.get(1)?,
content: row.get(2)?,
metadata: row.get(3)?,
embedding_blob: row.get(4)?,
created_at: row.get(5)?,
updated_at: row.get(6)?,
memory_type: row.get(7)?,
status: row.get(8)?,
superseded_by: row.get(9)?,
retrieval_count: row.get(10)?,
last_retrieved_at: row.get(11)?,
})
})?;
let mut results = Vec::new();
for row_result in rows {
results.push(row_result?);
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sqlite::embedding::{blob_to_vec, vec_to_blob};
use crate::sqlite::query_mod::map_row_to_memory;
use rusqlite::{Connection, params};
use tempfile::TempDir;
fn create_test_db() -> (TempDir, Database) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.db");
let db = Database::open(&path).unwrap();
(dir, db)
}
fn seed_raw(
conn: &Connection,
id: &str,
project_id: &str,
content: &str,
embedding: Option<&[u8]>,
retrieval_count: i64,
last_retrieved_at: Option<&str>,
) {
conn.execute(
r#"
INSERT INTO memories (id, project_id, content, embedding, metadata, created_at, updated_at, type, status, superseded_by, retrieval_count, last_retrieved_at)
VALUES (?1, ?2, ?3, ?4, NULL, '2024-01-01T00:00:00Z', '2024-01-01T00:00:00Z', 'fact', 'active', NULL, ?5, ?6)
"#,
params![
id,
project_id,
content,
embedding,
retrieval_count,
last_retrieved_at,
],
)
.unwrap();
}
#[test]
fn test_scan_all_rows_returns_every_row_across_projects_uncapped() {
let (_dir, db) = create_test_db();
let conn = db.conn();
let good_blob = vec_to_blob(&vec![0.5f32; 384]).unwrap();
seed_raw(conn, "a1", "projA", "content A", Some(&good_blob), 0, None);
seed_raw(
conn,
"b1",
"projB",
"content B",
Some(&good_blob),
3,
Some("2024-02-01T00:00:00Z"),
);
seed_raw(conn, "b2", "projB", "content C", Some(&good_blob), 0, None);
let rows = db.scan_all_rows().unwrap();
assert_eq!(rows.len(), 3);
let ids: Vec<_> = rows.iter().map(|r| r.id.clone()).collect();
assert_eq!(
ids,
vec!["a1".to_string(), "b1".to_string(), "b2".to_string()]
);
let b1 = &rows[1];
assert_eq!(b1.project_id, "projB");
assert_eq!(b1.content, "content B");
assert_eq!(b1.embedding_blob, good_blob);
assert_eq!(b1.retrieval_count, 3);
assert_eq!(
b1.last_retrieved_at,
Some("2024-02-01T00:00:00Z".to_string())
);
assert_eq!(b1.created_at, "2024-01-01T00:00:00Z");
assert_eq!(b1.updated_at, "2024-01-01T00:00:00Z");
assert_eq!(b1.memory_type, "fact");
assert_eq!(b1.status, "active");
assert_eq!(b1.superseded_by, None);
assert_eq!(b1.metadata, None);
}
#[test]
fn test_scan_all_rows_exports_corrupt_and_empty_blobs_without_error() {
let (_dir, db) = create_test_db();
let conn = db.conn();
let good_blob = vec_to_blob(&vec![0.5f32; 384]).unwrap();
seed_raw(conn, "good", "p", "g", Some(&good_blob), 0, None);
seed_raw(conn, "short", "p", "s", Some(&vec![0xABu8; 1535]), 0, None);
seed_raw(conn, "empty", "p", "e", Some(&[]), 0, None);
let rows = db.scan_all_rows().unwrap();
assert_eq!(rows.len(), 3);
let by_id = |id: &str| rows.iter().find(|r| r.id == id).unwrap();
assert_eq!(by_id("good").embedding_blob, good_blob);
assert_eq!(by_id("short").embedding_blob, vec![0xABu8; 1535]);
assert!(by_id("empty").embedding_blob.is_empty());
}
#[test]
fn test_scan_all_rows_does_not_use_capped_list_path() {
let (_dir, db) = create_test_db();
let conn = db.conn();
let good_blob = vec_to_blob(&vec![0.1f32; 384]).unwrap();
for i in 0..250 {
seed_raw(
conn,
&format!("id-{i}"),
"p",
&format!("content {i}"),
Some(&good_blob),
0,
None,
);
}
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM memories", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 250);
assert_eq!(db.scan_all_rows().unwrap().len(), 250);
}
#[test]
fn test_scan_all_rows_includes_non_active_statuses() {
let (_dir, db) = create_test_db();
let conn = db.conn();
let good_blob = vec_to_blob(&vec![0.5f32; 384]).unwrap();
seed_raw(conn, "s1", "p", "c", Some(&good_blob), 0, None);
conn.execute(
"UPDATE memories SET status = 'superseded', superseded_by = ?1 WHERE id = 's1'",
["s2"],
)
.unwrap();
let rows = db.scan_all_rows().unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].status, "superseded");
assert_eq!(rows[0].superseded_by, Some("s2".to_string()));
}
#[test]
fn test_insert_with_id_restores_all_12_columns_verbatim() {
let (_dir, db) = create_test_db();
let blob = vec_to_blob(&vec![0.25f32; 384]).unwrap();
db.insert_with_id(
"fixed-id-1",
"projX",
"restored content",
&blob,
Some(r#"{"k":"v"}"#),
"2023-05-05T05:05:05Z",
"2023-06-06T06:06:06Z",
"guard",
"candidate",
Some("other-id"),
42,
Some("2023-07-07T07:07:07Z"),
)
.unwrap();
let conn = db.conn();
let memory: crate::sqlite::Memory = conn
.query_row(
r#"
SELECT id, project_id, content, metadata, embedding, created_at, updated_at, type, status, superseded_by, retrieval_count, last_retrieved_at, importance
FROM memories WHERE id = 'fixed-id-1'
"#,
[],
map_row_to_memory,
)
.unwrap();
assert_eq!(memory.id, "fixed-id-1");
assert_eq!(memory.project_id, "projX");
assert_eq!(memory.content, "restored content");
assert_eq!(memory.metadata, Some(r#"{"k":"v"}"#.to_string()));
assert_eq!(memory.created_at, "2023-05-05T05:05:05Z");
assert_eq!(memory.updated_at, "2023-06-06T06:06:06Z");
assert_eq!(memory.memory_type, "guard");
assert_eq!(memory.status, "candidate");
assert_eq!(memory.superseded_by, Some("other-id".to_string()));
assert_eq!(memory.retrieval_count, 42);
assert_eq!(
memory.last_retrieved_at,
Some("2023-07-07T07:07:07Z".to_string())
);
let stored: Vec<u8> = conn
.query_row(
"SELECT embedding FROM memories WHERE id = 'fixed-id-1'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(stored, blob);
assert_eq!(blob_to_vec(&stored).unwrap(), vec![0.25f32; 384]);
}
#[test]
fn test_insert_with_id_duplicate_id_errors_without_upserting() {
let (_dir, db) = create_test_db();
let blob = vec_to_blob(&vec![0.5f32; 384]).unwrap();
db.insert_with_id(
"dup-id",
"p",
"original",
&blob,
None,
"2023-01-01T00:00:00Z",
"2023-01-01T00:00:00Z",
"fact",
"active",
None,
0,
None,
)
.unwrap();
let result = db.insert_with_id(
"dup-id",
"p",
"replacement",
&blob,
None,
"2024-01-01T00:00:00Z",
"2024-01-01T00:00:00Z",
"guard",
"candidate",
None,
9,
None,
);
assert!(result.is_err());
let (content, count): (String, i64) = db
.conn()
.query_row(
"SELECT content, retrieval_count FROM memories WHERE id = 'dup-id'",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap();
assert_eq!(content, "original");
assert_eq!(count, 0);
}
#[test]
fn test_insert_with_id_writes_raw_blob_undecoded() {
let (_dir, db) = create_test_db();
let weird = vec![0x99u8; 1535];
db.insert_with_id(
"raw-1",
"p",
"c",
&weird,
None,
"2023-01-01T00:00:00Z",
"2023-01-01T00:00:00Z",
"fact",
"active",
None,
0,
None,
)
.unwrap();
let stored: Vec<u8> = db
.conn()
.query_row(
"SELECT embedding FROM memories WHERE id = 'raw-1'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(stored, weird);
}
}