use std::time::SystemTime;
use super::Store;
use crate::error::MemoryError;
use crate::observation::{Observation, ObservationType, SaveEntry};
use crate::types::{format_sqlite_timestamp, parse_sqlite_timestamp};
use uuid::Uuid;
type ObservationTuple = (
String, String, String, String, Option<String>, Option<String>, Option<String>, Option<String>, String, String, );
fn tuple_to_observation(row: ObservationTuple) -> Result<Observation, MemoryError> {
let (id, sender_id, kind_str, title, what, why, where_field, learned, created_at, updated_at) =
row;
let kind = ObservationType::from_db_str(&kind_str).ok_or_else(|| {
MemoryError::logic(format!(
"observation row {id} carries unknown type `{kind_str}` not in the current ObservationType enum"
))
})?;
Ok(Observation {
id,
sender_id,
kind,
title,
what,
why,
where_field,
learned,
created_at: parse_sqlite_timestamp(&created_at)?,
updated_at: parse_sqlite_timestamp(&updated_at)?,
})
}
impl Store {
pub async fn save_observation(&self, entry: SaveEntry) -> Result<Observation, MemoryError> {
let id = Uuid::new_v4().to_string();
let now = SystemTime::now();
let now_str = format_sqlite_timestamp(now);
let kind_str = entry.kind.as_db_str();
sqlx::query(
"INSERT INTO observations \
(id, sender_id, type, title, what, why, where_field, learned, created_at, updated_at) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(&id)
.bind(&entry.sender_id)
.bind(kind_str)
.bind(&entry.title)
.bind(&entry.what)
.bind(&entry.why)
.bind(&entry.where_field)
.bind(&entry.learned)
.bind(&now_str)
.bind(&now_str)
.execute(&self.pool)
.await
.map_err(|e| MemoryError::sqlite("insert observation failed", e))?;
Ok(Observation {
id,
sender_id: entry.sender_id,
kind: entry.kind,
title: entry.title,
what: entry.what,
why: entry.why,
where_field: entry.where_field,
learned: entry.learned,
created_at: now,
updated_at: now,
})
}
pub async fn get_observation_by_id(
&self,
id: &str,
) -> Result<Option<Observation>, MemoryError> {
let row: Option<ObservationTuple> = sqlx::query_as(
"SELECT id, sender_id, type, title, what, why, where_field, learned, \
created_at, updated_at \
FROM observations \
WHERE id = ? AND deleted_at IS NULL",
)
.bind(id)
.fetch_optional(&self.pool)
.await
.map_err(|e| MemoryError::sqlite("get_observation_by_id failed", e))?;
match row {
Some(tuple) => Ok(Some(tuple_to_observation(tuple)?)),
None => Ok(None),
}
}
pub async fn search_observations(
&self,
query: &str,
sender_id: &str,
limit: i64,
since: Option<SystemTime>,
kind: Option<ObservationType>,
) -> Result<Vec<Observation>, MemoryError> {
if query.len() < 3 {
return Ok(Vec::new());
}
let sanitized = format!("\"{}\"", query.replace('"', "\"\""));
let mut sql = String::from(
"SELECT o.id, o.sender_id, o.type, o.title, o.what, o.why, \
o.where_field, o.learned, o.created_at, o.updated_at \
FROM observations_fts fts \
JOIN observations o ON o.rowid = fts.rowid \
WHERE observations_fts MATCH ? \
AND o.sender_id = ? \
AND o.deleted_at IS NULL",
);
if since.is_some() {
sql.push_str(" AND o.created_at >= ?");
}
if kind.is_some() {
sql.push_str(" AND o.type = ?");
}
sql.push_str(" ORDER BY rank, o.created_at DESC LIMIT ?");
let mut q = sqlx::query_as::<_, ObservationTuple>(&sql)
.bind(&sanitized)
.bind(sender_id);
if let Some(cutoff) = since {
q = q.bind(format_sqlite_timestamp(cutoff));
}
if let Some(k) = kind {
q = q.bind(k.as_db_str());
}
let rows: Vec<ObservationTuple> = q
.bind(limit)
.fetch_all(&self.pool)
.await
.map_err(|e| MemoryError::sqlite("search_observations failed", e))?;
let mut out = Vec::with_capacity(rows.len());
for tuple in rows {
out.push(tuple_to_observation(tuple)?);
}
Ok(out)
}
pub async fn soft_delete_observation(&self, id: &str) -> Result<bool, MemoryError> {
let now_str = format_sqlite_timestamp(SystemTime::now());
let result = sqlx::query(
"UPDATE observations SET deleted_at = ?, updated_at = ? \
WHERE id = ? AND deleted_at IS NULL",
)
.bind(&now_str)
.bind(&now_str)
.bind(id)
.execute(&self.pool)
.await
.map_err(|e| MemoryError::sqlite("soft delete observation failed", e))?;
Ok(result.rows_affected() > 0)
}
pub async fn list_soft_deleted_observations(
&self,
sender_id: &str,
) -> Result<Vec<Observation>, MemoryError> {
let rows: Vec<ObservationTuple> = sqlx::query_as(
"SELECT id, sender_id, type, title, what, why, where_field, learned, \
created_at, updated_at \
FROM observations \
WHERE sender_id = ? AND deleted_at IS NOT NULL \
ORDER BY deleted_at DESC",
)
.bind(sender_id)
.fetch_all(&self.pool)
.await
.map_err(|e| MemoryError::sqlite("list_soft_deleted_observations failed", e))?;
let mut out = Vec::with_capacity(rows.len());
for tuple in rows {
out.push(tuple_to_observation(tuple)?);
}
Ok(out)
}
pub(crate) async fn count_observations(&self, sender_id: &str) -> Result<i64, MemoryError> {
let (count,): (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM observations \
WHERE sender_id = ? AND deleted_at IS NULL",
)
.bind(sender_id)
.fetch_one(&self.pool)
.await
.map_err(|e| MemoryError::sqlite("count_observations failed", e))?;
Ok(count)
}
}