#![allow(dead_code)]
use anyhow::{Context, Result};
use chrono::Utc;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub id: String,
pub user_id: String,
pub text: String,
pub scope: String, pub priority: String, pub source: String, pub project: Option<String>,
pub tech: Option<String>,
pub metadata: serde_json::Value,
pub status: String, pub created_at: String,
pub updated_at: String,
pub deleted_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MemoryInput {
pub user_id: String,
pub text: String,
pub scope: String,
pub priority: String,
#[serde(default = "default_source")]
pub source: String,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub tech: Option<String>,
#[serde(default = "default_metadata")]
pub metadata: serde_json::Value,
}
fn default_source() -> String {
"asurada".into()
}
fn default_metadata() -> serde_json::Value {
serde_json::json!({})
}
pub fn insert(conn: &Connection, input: MemoryInput) -> Result<Memory> {
let id = super::uuid_like();
let now = Utc::now().to_rfc3339();
conn.execute(
r#"INSERT INTO memories
(id, user_id, text, scope, priority, source, project, tech, metadata, status, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 'active', ?10, ?10)"#,
params![
id,
input.user_id,
input.text,
input.scope,
input.priority,
input.source,
input.project,
input.tech,
input.metadata.to_string(),
now,
],
)
.context("insert memory")?;
get(conn, &input.user_id, &id)?.context("memory disappeared after insert")
}
pub fn get(conn: &Connection, user_id: &str, id: &str) -> Result<Option<Memory>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, text, scope, priority, source, project, tech, metadata,
status, created_at, updated_at, deleted_at
FROM memories
WHERE user_id = ?1 AND id = ?2 AND status != 'deleted'"#,
)?;
let row = stmt.query_row(params![user_id, id], row_to_memory).ok();
Ok(row)
}
pub fn list(
conn: &Connection,
user_id: &str,
scope: Option<&str>,
limit: usize,
) -> Result<Vec<Memory>> {
let mut sql = String::from(
r#"SELECT id, user_id, text, scope, priority, source, project, tech, metadata,
status, created_at, updated_at, deleted_at
FROM memories
WHERE user_id = ?1 AND status = 'active'"#,
);
if scope.is_some() {
sql.push_str(" AND scope = ?2");
}
sql.push_str(" ORDER BY updated_at DESC LIMIT ?_LIM_");
let sql = sql.replace("?_LIM_", if scope.is_some() { "?3" } else { "?2" });
let mut stmt = conn.prepare(&sql)?;
let rows: Vec<Memory> = if let Some(s) = scope {
stmt.query_map(params![user_id, s, limit as i64], row_to_memory)?
.filter_map(|r| r.ok())
.collect()
} else {
stmt.query_map(params![user_id, limit as i64], row_to_memory)?
.filter_map(|r| r.ok())
.collect()
};
Ok(rows)
}
pub fn search(conn: &Connection, user_id: &str, query: &str, limit: usize) -> Result<Vec<Memory>> {
let mut stmt = conn.prepare(
r#"SELECT m.id, m.user_id, m.text, m.scope, m.priority, m.source, m.project, m.tech,
m.metadata, m.status, m.created_at, m.updated_at, m.deleted_at
FROM memories m
JOIN memories_fts f ON f.rowid = m.rowid
WHERE f.text MATCH ?1
AND m.user_id = ?2
AND m.status = 'active'
ORDER BY rank
LIMIT ?3"#,
)?;
let rows: Vec<Memory> = stmt
.query_map(
params![sanitize_fts(query), user_id, limit as i64],
row_to_memory,
)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn soft_delete(conn: &Connection, user_id: &str, id: &str) -> Result<bool> {
let now = Utc::now().to_rfc3339();
let n = conn.execute(
r#"UPDATE memories SET status = 'deleted', deleted_at = ?1, updated_at = ?1
WHERE user_id = ?2 AND id = ?3 AND status != 'deleted'"#,
params![now, user_id, id],
)?;
Ok(n > 0)
}
fn row_to_memory(row: &rusqlite::Row<'_>) -> rusqlite::Result<Memory> {
let metadata_str: String = row.get(8)?;
let metadata = serde_json::from_str(&metadata_str).unwrap_or_else(|_| serde_json::json!({}));
Ok(Memory {
id: row.get(0)?,
user_id: row.get(1)?,
text: row.get(2)?,
scope: row.get(3)?,
priority: row.get(4)?,
source: row.get(5)?,
project: row.get(6)?,
tech: row.get(7)?,
metadata,
status: row.get(9)?,
created_at: row.get(10)?,
updated_at: row.get(11)?,
deleted_at: row.get(12)?,
})
}
fn sanitize_fts(q: &str) -> String {
let words: Vec<String> = q
.split_whitespace()
.map(|w| {
w.chars()
.filter(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>()
})
.filter(|w| !w.is_empty())
.collect();
if words.is_empty() {
return "\"\"".into();
}
words.join(" OR ")
}