pub mod embedding;
pub mod fts;
pub mod list;
pub mod migrations;
pub mod query_mod;
pub mod search;
pub mod supersede;
pub mod update;
use chrono::Utc;
use rusqlite::{Connection, OptionalExtension, params};
use std::path::Path;
use uuid::Uuid;
pub use self::embedding::{blob_to_vec, vec_to_blob};
pub use self::query_mod::map_row_to_memory;
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub struct Memory {
pub id: String,
pub project_id: String,
pub content: String,
pub metadata: Option<String>,
pub embedding: Vec<f32>,
pub similarity: Option<f64>,
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>,
}
#[derive(Debug)]
pub enum Error {
Sqlite(String),
InvalidBlobSize { expected: usize, actual: usize },
MismatchedDimensions { expected: usize, actual: usize },
EmptyVector,
InvalidEmbedding(String),
InvalidLimit(String),
NotFound(String),
InvalidInput(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Sqlite(msg) => write!(f, "Database error: {}", msg),
Error::InvalidBlobSize { expected, actual } => {
write!(
f,
"Invalid BLOB size: expected {} bytes, got {} bytes",
expected, actual
)
}
Error::MismatchedDimensions { expected, actual } => {
write!(
f,
"Mismatched dimensions: expected {} dimensions, got {} dimensions",
expected, actual
)
}
Error::EmptyVector => write!(f, "Cannot compute similarity with empty vector"),
Error::InvalidEmbedding(msg) => write!(f, "Invalid embedding: {}", msg),
Error::InvalidLimit(msg) => write!(f, "Invalid limit: {}", msg),
Error::NotFound(msg) => write!(f, "Not found: {}", msg),
Error::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}
impl std::error::Error for Error {}
impl From<rusqlite::Error> for Error {
fn from(err: rusqlite::Error) -> Self {
Error::Sqlite(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Database {
conn: Connection,
}
fn create_schema(conn: &mut Connection) -> Result<()> {
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding BLOB NOT NULL,
metadata TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_id);
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
content,
project_id UNINDEXED,
tokenize='porter unicode61',
content_rowid='rowid',
content='memories'
);
CREATE TRIGGER IF NOT EXISTS memories_fts_insert AFTER INSERT ON memories BEGIN
INSERT INTO memories_fts(rowid, content, project_id)
VALUES (new.rowid, new.content, new.project_id);
END;
CREATE TRIGGER IF NOT EXISTS memories_fts_delete AFTER DELETE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, content, project_id)
VALUES('delete', old.rowid, old.content, old.project_id);
END;
CREATE TRIGGER IF NOT EXISTS memories_fts_update AFTER UPDATE ON memories BEGIN
INSERT INTO memories_fts(memories_fts, rowid, content, project_id)
VALUES('delete', old.rowid, old.content, old.project_id);
INSERT INTO memories_fts(rowid, content, project_id)
VALUES (new.rowid, new.content, new.project_id);
END;
"#,
)?;
Ok(())
}
impl Database {
pub fn open(path: &Path) -> Result<Self> {
let mut conn = Connection::open(path)?;
create_schema(&mut conn)?;
migrations::run_migrations(&conn)?;
Ok(Self { conn })
}
pub fn insert(
&self,
project_id: &str,
content: &str,
embedding: &[f32],
metadata: Option<&str>,
memory_type: &str,
status: &str,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339();
let blob = vec_to_blob(embedding)?;
self.conn.execute(
r#"
INSERT INTO memories (id, project_id, content, embedding, metadata, created_at, updated_at, type, status)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
"#,
params![&id, project_id, content, &blob, metadata, &now, &now, memory_type, status],
)?;
Ok(id)
}
#[cfg(test)]
pub(crate) fn insert_with_time(
&self,
project_id: &str,
content: &str,
embedding: &[f32],
metadata: Option<&str>,
created_at: &str,
updated_at: &str,
memory_type: &str,
status: &str,
) -> Result<String> {
let id = Uuid::new_v4().to_string();
let blob = vec_to_blob(embedding)?;
self.conn.execute(
r#"
INSERT INTO memories (id, project_id, content, embedding, metadata, created_at, updated_at, type, status)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
"#,
params![&id, project_id, content, &blob, metadata, created_at, updated_at, memory_type, status],
)?;
Ok(id)
}
pub fn get(&self, id: &str, project_id: &str) -> Result<Option<Memory>> {
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
FROM memories
WHERE id = ?1 AND project_id = ?2
"#,
)?;
let result = stmt
.query_row([id, project_id], map_row_to_memory)
.optional()?;
Ok(result)
}
pub fn delete(&self, id: &str, project_id: &str) -> Result<bool> {
let rows = self.conn.execute(
"DELETE FROM memories WHERE id = ?1 AND project_id = ?2",
[id, project_id],
)?;
Ok(rows > 0)
}
#[cfg(test)]
pub(crate) fn conn(&self) -> &Connection {
&self.conn
}
}