use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::error::{DbError, Result};
use crate::graph::builder::AttributeMode;
use crate::temporal::replay::PAYLOAD_VERSION;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeAttributes {
pub id: String,
pub title: String,
pub content: String,
pub embedding_model: Option<String>,
}
use crate::util::limits::HYDRATE_CHUNK;
pub async fn query_as_of_edges(
conn: &libsql::Connection,
ts: &str,
) -> Result<Vec<(String, String, String, String, String)>> {
let sql = r#"
SELECT source_id, target_id, edge_type, valid_from, valid_to
FROM links_current
WHERE valid_from <= ?1 AND ?1 < valid_to
"#;
let mut rows = conn.query(sql, libsql::params![ts]).await?;
let mut edges = Vec::new();
while let Some(row) = rows.next().await? {
let src: String = row.get(0)?;
let tgt: String = row.get(1)?;
let edge_type: String = row.get(2)?;
let vf: String = row.get(3)?;
let vt: String = row.get(4)?;
edges.push((src, tgt, edge_type, vf, vt));
}
Ok(edges)
}
fn placeholders(first: usize, count: usize) -> String {
(first..first + count)
.map(|i| format!("?{i}"))
.collect::<Vec<_>>()
.join(", ")
}
pub async fn hydrate_attributes(
conn: &libsql::Connection,
node_ids: &[String],
ts: &str,
mode: AttributeMode,
) -> Result<Vec<NodeAttributes>> {
if node_ids.is_empty() {
return Ok(Vec::new());
}
let found: HashMap<String, NodeAttributes> = match mode {
AttributeMode::Omit => return Ok(Vec::new()),
AttributeMode::Current => hydrate_current(conn, node_ids).await?,
AttributeMode::AtTime => hydrate_at_time(conn, node_ids, ts).await?,
};
let mut out = Vec::with_capacity(found.len());
for id in node_ids {
if let Some(attrs) = found.get(id) {
out.push(attrs.clone());
}
}
Ok(out)
}
async fn hydrate_current(
conn: &libsql::Connection,
node_ids: &[String],
) -> Result<HashMap<String, NodeAttributes>> {
let mut found = HashMap::new();
for chunk in node_ids.chunks(HYDRATE_CHUNK) {
let sql = format!(
"SELECT id, title, content, embedding_model FROM concepts \
WHERE retired = 0 AND id IN ({})",
placeholders(1, chunk.len())
);
let params: Vec<libsql::Value> = chunk
.iter()
.map(|id| libsql::Value::Text(id.clone()))
.collect();
let mut rows = conn.query(&sql, params).await?;
while let Some(row) = rows.next().await? {
let id: String = row.get(0)?;
found.insert(
id.clone(),
NodeAttributes {
id,
title: row.get(1)?,
content: row.get(2)?,
embedding_model: row.get(3).ok(),
},
);
}
}
Ok(found)
}
async fn hydrate_at_time(
conn: &libsql::Connection,
node_ids: &[String],
ts: &str,
) -> Result<HashMap<String, NodeAttributes>> {
let mut found = HashMap::new();
for chunk in node_ids.chunks(HYDRATE_CHUNK) {
let sql = format!(
r#"
SELECT entity_id, seq_id, payload FROM (
SELECT entity_id, seq_id, payload,
ROW_NUMBER() OVER (PARTITION BY entity_id ORDER BY seq_id DESC) as rn
FROM transaction_log
WHERE table_name = 'concepts'
AND recorded_at <= ?1
AND entity_id IN ({})
) WHERE rn = 1
"#,
placeholders(2, chunk.len())
);
let mut params: Vec<libsql::Value> = Vec::with_capacity(chunk.len() + 1);
params.push(libsql::Value::Text(ts.to_string()));
params.extend(chunk.iter().map(|id| libsql::Value::Text(id.clone())));
let mut rows = conn.query(&sql, params).await?;
while let Some(row) = rows.next().await? {
let id: String = row.get(0)?;
let seq_id: i64 = row.get(1)?;
let payload_str: String = row.get(2)?;
let payload: serde_json::Value =
serde_json::from_str(&payload_str).map_err(|e| DbError::ReplayCorrupt {
seq: seq_id,
reason: format!("Failed to parse payload JSON: {e}"),
})?;
let v = payload.get("v").and_then(|v| v.as_u64()).unwrap_or(1);
if v > PAYLOAD_VERSION as u64 {
return Err(DbError::PayloadVersion {
got: v as u8,
max: PAYLOAD_VERSION,
});
}
if payload.get("retired").and_then(|r| r.as_i64()).unwrap_or(0) != 0 {
continue;
}
found.insert(
id.clone(),
NodeAttributes {
id,
title: payload
.get("title")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string(),
content: payload
.get("content")
.and_then(|s| s.as_str())
.unwrap_or("")
.to_string(),
embedding_model: payload
.get("embedding_model")
.and_then(|s| s.as_str())
.map(|s| s.to_string()),
},
);
}
}
Ok(found)
}