use postgres::Client;
pub const VEC_DIM: usize = 384;
const MIGRATIONS_TABLE: &str = r#"
CREATE TABLE IF NOT EXISTS migrations (
id TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;
pub const MIGRATIONS: &[(&str, &str)] = &[("001_schema", include_str!("schema.sql"))];
pub struct MigrationReport {
pub applied: Vec<String>,
pub skipped: Vec<String>,
}
pub fn pg_url() -> String {
std::env::var("LEANKG_PG_URL")
.unwrap_or_else(|_| "postgresql://postgres:postgres@localhost:5433/leankg".to_string())
}
pub fn run_migrations(client: &mut Client) -> Result<MigrationReport, postgres::Error> {
client.batch_execute(MIGRATIONS_TABLE)?;
let mut applied = Vec::new();
let mut skipped = Vec::new();
for (version, sql) in MIGRATIONS {
let already: bool = client
.query_one(
"SELECT EXISTS (SELECT 1 FROM migrations WHERE id = $1)",
&[&version],
)?
.get(0);
if already {
skipped.push((*version).to_string());
continue;
}
let mut tx = client.transaction()?;
tx.batch_execute(sql)?;
tx.execute("INSERT INTO migrations (id) VALUES ($1)", &[&version])?;
tx.commit()?;
applied.push((*version).to_string());
}
Ok(MigrationReport { applied, skipped })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn migrations_are_unique_and_ordered() {
let mut prev: Option<&str> = None;
for (version, sql) in MIGRATIONS {
assert!(!version.is_empty(), "migration id must not be empty");
assert!(!sql.trim().is_empty(), "migration {version} has empty SQL");
if let Some(p) = prev {
assert!(*version > p, "migration {version} out of order after {p}");
}
prev = Some(version);
}
}
#[test]
fn schema_sql_matches_vec_dim_const() {
let schema = include_str!("schema.sql");
assert!(
schema.contains(&format!("vector({VEC_DIM})")),
"schema.sql must declare vector({VEC_DIM})"
);
}
#[test]
fn schema_sql_has_no_query_cache_table() {
let schema = include_str!("schema.sql");
for line in schema.lines() {
let t = line.trim();
if t.starts_with("CREATE TABLE") && t.contains("query_cache") {
panic!("query_cache dropped per D2 — schema.sql creates it: {t}");
}
}
}
}