use anyhow::Result;
use chrono::Utc;
use rusqlite::{params, Transaction};
use serde_json::Value;
pub fn migrate_v1_execution(tx: &Transaction<'_>) -> Result<()> {
tx.execute_batch(
"CREATE TABLE IF NOT EXISTS agents (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
project_id TEXT,
metadata TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS agents_project_idx ON agents(project_id);
CREATE TABLE IF NOT EXISTS reasoning_logs (
id INTEGER PRIMARY KEY,
agent_id INTEGER NOT NULL REFERENCES agents(id),
content TEXT NOT NULL,
project_id TEXT,
metadata TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS reasoning_logs_agent_idx ON reasoning_logs(agent_id);
CREATE INDEX IF NOT EXISTS reasoning_logs_project_idx ON reasoning_logs(project_id);
CREATE INDEX IF NOT EXISTS reasoning_logs_created_at_idx ON reasoning_logs(created_at);
CREATE TABLE IF NOT EXISTS tool_calls (
id INTEGER PRIMARY KEY,
reasoning_log_id INTEGER NOT NULL REFERENCES reasoning_logs(id),
tool_name TEXT NOT NULL,
args TEXT NOT NULL,
project_id TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS tool_calls_log_idx ON tool_calls(reasoning_log_id);
CREATE INDEX IF NOT EXISTS tool_calls_tool_name_idx ON tool_calls(tool_name);
CREATE INDEX IF NOT EXISTS tool_calls_project_idx ON tool_calls(project_id);",
)?;
backfill_agents(tx)?;
backfill_reasoning_logs(tx)?;
backfill_tool_calls(tx)?;
Ok(())
}
fn backfill_agents(tx: &Transaction<'_>) -> Result<()> {
let mut stmt = tx.prepare(
"SELECT id, name, data FROM graph_entities
WHERE kind = 'Agent'
AND (data IS NULL OR json_extract(data, '$.sql_id') IS NULL)",
)?;
let rows: Vec<(i64, String, String)> = stmt
.query_map([], |r| {
Ok((
r.get::<_, i64>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
drop(stmt);
for (entity_id, name, data_str) in rows {
let data: Value = serde_json::from_str(&data_str).unwrap_or(Value::Null);
let project_id = data.get("project_id").and_then(|v| v.as_str());
tx.execute(
"INSERT OR IGNORE INTO agents (name, project_id, metadata, created_at)
VALUES (?1, ?2, ?3, ?4)",
params![
name,
project_id,
super::json_to_string(&data)?,
Utc::now().to_rfc3339()
],
)?;
let sql_id: i64 = tx.query_row(
"SELECT id FROM agents WHERE name = ?1",
params![name],
|r| r.get(0),
)?;
super::stamp_sql_id(tx, entity_id, &data, sql_id)?;
}
Ok(())
}
fn backfill_reasoning_logs(tx: &Transaction<'_>) -> Result<()> {
let mut stmt = tx.prepare(
"SELECT id, data FROM graph_entities
WHERE kind = 'ReasoningLog'
AND (data IS NULL OR json_extract(data, '$.sql_id') IS NULL)",
)?;
let rows: Vec<(i64, String)> = stmt
.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))?
.collect::<Result<Vec<_>, _>>()?;
drop(stmt);
for (entity_id, data_str) in rows {
let data: Value = serde_json::from_str(&data_str).unwrap_or(Value::Null);
let agent_name = data
.get("agent")
.and_then(|v| v.as_str())
.unwrap_or("_legacy");
let content = data.get("content").and_then(|v| v.as_str()).unwrap_or("");
let project_id = data.get("project_id").and_then(|v| v.as_str());
tx.execute(
"INSERT OR IGNORE INTO agents (name, project_id, metadata, created_at)
VALUES (?1, NULL, '{}', ?2)",
params![agent_name, Utc::now().to_rfc3339()],
)?;
let agent_id: i64 = tx.query_row(
"SELECT id FROM agents WHERE name = ?1",
params![agent_name],
|r| r.get(0),
)?;
tx.execute(
"INSERT INTO reasoning_logs
(agent_id, content, project_id, metadata, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
agent_id,
content,
project_id,
super::json_to_string(&data)?,
data.get("timestamp")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| Utc::now().to_rfc3339()),
],
)?;
let sql_id = tx.last_insert_rowid();
super::stamp_sql_id(tx, entity_id, &data, sql_id)?;
}
Ok(())
}
fn backfill_tool_calls(tx: &Transaction<'_>) -> Result<()> {
let mut stmt = tx.prepare(
"SELECT id, data FROM graph_entities
WHERE kind = 'ToolCall'
AND (data IS NULL OR json_extract(data, '$.sql_id') IS NULL)",
)?;
let rows: Vec<(i64, String)> = stmt
.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?)))?
.collect::<Result<Vec<_>, _>>()?;
drop(stmt);
for (entity_id, data_str) in rows {
let data: Value = serde_json::from_str(&data_str).unwrap_or(Value::Null);
let tool_name = data.get("tool_name").and_then(|v| v.as_str()).unwrap_or("");
let args = data.get("args").cloned().unwrap_or(Value::Null);
let project_id = data.get("project_id").and_then(|v| v.as_str());
let log_sql_id: Option<i64> = tx
.query_row(
"SELECT json_extract(le.data, '$.sql_id')
FROM graph_edges e
JOIN graph_entities le ON le.id = e.from_id
WHERE e.to_id = ?1
AND e.edge_type = 'called'
AND le.kind = 'ReasoningLog'
LIMIT 1",
params![entity_id],
|r| r.get::<_, Option<i64>>(0),
)
.ok()
.flatten();
let Some(log_sql_id) = log_sql_id else {
continue;
};
tx.execute(
"INSERT INTO tool_calls
(reasoning_log_id, tool_name, args, project_id, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
log_sql_id,
tool_name,
super::json_to_string(&args)?,
project_id,
data.get("timestamp")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(|| Utc::now().to_rfc3339()),
],
)?;
let sql_id = tx.last_insert_rowid();
super::stamp_sql_id(tx, entity_id, &data, sql_id)?;
}
Ok(())
}