use std::env;
use std::fs;
use std::path::PathBuf;
fn schema_user_version_from_sql(sql: &str) -> u32 {
for line in sql.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("PRAGMA user_version") {
let rest = rest.trim().trim_start_matches('=').trim().trim_end_matches(';').trim();
if let Ok(v) = rest.parse::<u32>() {
return v;
}
}
}
0
}
fn decode_user_version_to_semver(user_version: u32) -> String {
let major = user_version / 1_000_000;
let minor = (user_version % 1_000_000) / 1_000;
let patch = user_version % 1_000;
format!("{major}.{minor}.{patch}")
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let schema_dir = manifest_dir.join("..").join("..").join("schema");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
for name in ["duc.sql", "version_control.sql", "search.sql"] {
let src = schema_dir.join(name);
let dst = out_dir.join(name);
match fs::read_to_string(&src) {
Ok(contents) => fs::write(&dst, contents)?,
Err(e) => {
eprintln!("cargo:warning=Could not read {:?}: {e}. Writing empty stub.", src);
fs::write(&dst, "")?;
}
}
println!("cargo:rerun-if-changed={}", src.display());
}
let migrations_dir = schema_dir.join("migrations");
let mut migrations: Vec<(i64, i64, String)> = Vec::new();
if migrations_dir.is_dir() {
for entry in fs::read_dir(&migrations_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("sql") {
continue;
}
let stem = match path.file_stem().and_then(|s| s.to_str()) {
Some(s) => s.to_owned(),
None => continue,
};
if let Some((from_str, to_str)) = stem.split_once("_to_") {
if let (Ok(from), Ok(to)) = (from_str.parse::<i64>(), to_str.parse::<i64>()) {
let sql = fs::read_to_string(&path)?;
migrations.push((from, to, sql));
println!("cargo:rerun-if-changed={}", path.display());
} else {
eprintln!("cargo:warning=Skipping migration file with non-integer versions: {stem}.sql");
}
} else {
eprintln!("cargo:warning=Skipping migration file with unexpected name: {stem}.sql");
}
}
println!("cargo:rerun-if-changed={}", migrations_dir.display());
}
migrations.sort_by_key(|(from, _, _)| *from);
let mut reg = String::from(
"// Auto-generated by build.rs — do not edit by hand.\n\
// Each entry: (from_version, to_version, sql).\n\
pub(crate) static MIGRATIONS: &[(i64, i64, &str)] = &[\n",
);
for (from, to, sql) in &migrations {
reg.push_str(&format!(" ({from}i64, {to}i64, \""));
for c in sql.chars() {
match c {
'"' => reg.push_str("\\\""),
'\\' => reg.push_str("\\\\"),
'\r' => {} c => reg.push(c),
}
}
reg.push_str("\"),\n");
}
reg.push_str("];\n");
fs::write(out_dir.join("migrations_registry.rs"), reg)?;
let sql_path = schema_dir.join("duc.sql");
let (user_version, semver_version) = match fs::read_to_string(&sql_path) {
Ok(sql) => {
let uv = schema_user_version_from_sql(&sql);
(uv, decode_user_version_to_semver(uv))
}
Err(e) => {
eprintln!("cargo:warning=Could not read {:?}: {e}. Defaulting to 0.0.0.", sql_path);
(0, "0.0.0".to_string())
}
};
println!("cargo:rustc-env=DUC_SCHEMA_VERSION={semver_version}");
println!("cargo:rustc-env=DUC_SCHEMA_USER_VERSION={user_version}");
fs::write(
out_dir.join("schema_user_version.rs"),
format!("{}i32", user_version),
)?;
println!("cargo:rerun-if-changed=build.rs");
Ok(())
}