use std::error::Error;
use std::path::PathBuf;
use clap::Parser;
use llm_kernel::graph::store::list_node_ids;
use llm_kernel::graph::{
GraphBackend, GraphEdge, GraphNode, PgGraph, SqliteGraph, init_graph_schema, read_edges,
read_nodes,
};
use rusqlite::Connection;
const LIST_LIMIT: usize = 5_000_000;
#[derive(Parser, Debug)]
#[command(
name = "llm-kernel-migrate-graph",
about = "Migrate an llm-kernel graph between SQLite and PostgreSQL backends"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(clap::Subcommand, Debug)]
enum Command {
Migrate {
#[arg(long)]
from: String,
#[arg(long)]
to: String,
#[arg(long)]
dry_run: bool,
},
}
enum Endpoint {
Sqlite(PathBuf),
Postgres(String),
}
impl Endpoint {
fn parse(s: &str) -> Result<Self, Box<dyn Error>> {
if let Some(p) = s.strip_prefix("sqlite:") {
Ok(Self::Sqlite(PathBuf::from(p)))
} else {
Ok(Self::Postgres(s.to_string()))
}
}
fn describe(&self) -> &'static str {
match self {
Self::Sqlite(_) => "sqlite",
Self::Postgres(_) => "postgres",
}
}
}
fn read_source(from: &Endpoint) -> Result<(Vec<GraphNode>, Vec<GraphEdge>), Box<dyn Error>> {
Ok(match from {
Endpoint::Sqlite(p) => {
let conn = Connection::open(p)?;
init_graph_schema(&conn)?;
let ids = list_node_ids(&conn)?;
let refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
let nodes = read_nodes(&conn, &refs)?;
let edges = read_edges(&conn, LIST_LIMIT)?;
(nodes, edges)
}
Endpoint::Postgres(c) => {
let pg = PgGraph::connect(c)?;
let nodes = pg.list_nodes(LIST_LIMIT)?;
let edges = pg.list_edges(LIST_LIMIT)?;
(nodes, edges)
}
})
}
fn write_target(
to: &Endpoint,
nodes: &[GraphNode],
edges: &[GraphEdge],
) -> Result<(), Box<dyn Error>> {
let backend: Box<dyn GraphBackend> = match to {
Endpoint::Sqlite(p) => Box::new(SqliteGraph::open(p)?),
Endpoint::Postgres(c) => Box::new(PgGraph::connect(c)?),
};
for n in nodes {
backend.upsert_node(n)?;
}
for e in edges {
backend.append_edge(e)?;
}
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse();
let Command::Migrate { from, to, dry_run } = cli.command;
let from = Endpoint::parse(&from)?;
let to = Endpoint::parse(&to)?;
let (nodes, edges) = read_source(&from)?;
eprintln!(
"source ({}) -> {} nodes, {} edges",
from.describe(),
nodes.len(),
edges.len()
);
if dry_run {
eprintln!("dry-run: would write to {}", to.describe());
if let Some(n) = nodes.first() {
eprintln!("sample node: id={} title={}", n.id, n.title);
}
return Ok(());
}
write_target(&to, &nodes, &edges)?;
eprintln!(
"migrated {} nodes + {} edges to {}",
nodes.len(),
edges.len(),
to.describe()
);
Ok(())
}