#![deny(unsafe_code)]
use roomrs_migrate::{SchemaSnapshot, diff_plan, diff_sql, list_snapshot_versions};
use std::path::Path;
use std::process::ExitCode;
fn main() -> ExitCode {
let mut args: Vec<String> = Vec::new();
for a in std::env::args_os().skip(1) {
match a.into_string() {
Ok(s) => args.push(s),
Err(bad) => {
eprintln!("인자가 유효한 UTF-8이 아닙니다: {}", bad.to_string_lossy());
return ExitCode::from(2);
}
}
}
let strict = args.iter().any(|a| a == "--strict");
let strs: Vec<&str> = args
.iter()
.map(String::as_str)
.filter(|a| *a != "--strict")
.collect();
match strs.as_slice() {
["migrate", "diff", old, new] => cmd_diff(old, new, None),
["migrate", "diff", old, new, out] => cmd_diff(old, new, Some(out)),
["migrate", "check", a, b] => cmd_check(a, b),
["migrate", "check-dir", dir, db] => cmd_check_dir(dir, db, strict),
_ => {
eprintln!(
"사용법:\n roomrs migrate diff <old.json> <new.json> [out.sql] # 초안 생성\n roomrs migrate check <a.json> <b.json> # 스냅샷 해시 비교\n roomrs migrate check-dir <schema_dir> <db이름> [--strict] # 버전 파일 스캔 검증\n # --strict: 경고(버전 갭·파괴적 변경)를 exit 1로 승격"
);
ExitCode::from(2)
}
}
}
fn load(path: &str) -> Result<SchemaSnapshot, ExitCode> {
SchemaSnapshot::read_from(Path::new(path)).map_err(|e| {
eprintln!("스냅샷을 읽을 수 없습니다 ({path}): {e}");
ExitCode::FAILURE
})
}
fn cmd_diff(old: &str, new: &str, out: Option<&str>) -> ExitCode {
let (old, new) = match (load(old), load(new)) {
(Ok(a), Ok(b)) => (a, b),
(Err(c), _) | (_, Err(c)) => return c,
};
let sql = diff_sql(&old, &new);
match out {
None => {
print!("{sql}");
ExitCode::SUCCESS
}
Some(path) => match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
{
Ok(mut f) => match std::io::Write::write_all(&mut f, sql.as_bytes()) {
Ok(()) => {
eprintln!("초안 저장: {path}");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("초안 저장 실패 ({path}): {e}");
ExitCode::FAILURE
}
},
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
eprintln!(
"초안 파일이 이미 존재합니다 ({path}) — 덮어쓰지 않습니다. 확인 후 삭제하고 다시 실행하세요"
);
ExitCode::FAILURE
}
Err(e) => {
eprintln!("초안 저장 실패 ({path}): {e}");
ExitCode::FAILURE
}
},
}
}
fn cmd_check_dir(dir: &str, db: &str, strict: bool) -> ExitCode {
let files = match list_snapshot_versions(Path::new(dir), db) {
Ok(f) => f,
Err(e) => {
eprintln!("스냅샷 디렉토리를 읽을 수 없습니다 ({dir}): {e}");
return ExitCode::FAILURE;
}
};
if files.is_empty() {
eprintln!("스냅샷이 없습니다: {dir} 에 {db}.<버전>.json 파일 없음");
return ExitCode::FAILURE;
}
let mut snaps: Vec<SchemaSnapshot> = Vec::with_capacity(files.len());
for (ver, path) in &files {
let s = match SchemaSnapshot::read_from(path) {
Ok(s) => s,
Err(e) => {
eprintln!("스냅샷을 읽을 수 없습니다 ({}): {e}", path.display());
return ExitCode::FAILURE;
}
};
if s.version != *ver {
eprintln!(
"스냅샷 내부 version({})이 파일명 버전({ver})과 다릅니다: {}",
s.version,
path.display()
);
return ExitCode::FAILURE;
}
snaps.push(s);
}
let mut warn_count: usize = 0;
for pair in snaps.windows(2) {
if pair[1].version - pair[0].version > 1 {
eprintln!(
"경고: 버전 갭: v{} 다음 v{} — 중간 스냅샷 누락(내장 자동 마이그레이션 불가 구간)",
pair[0].version, pair[1].version
);
warn_count += 1;
}
let plan = diff_plan(&pair[0], &pair[1]);
for d in &plan.destructive {
eprintln!(
"경고: v{} -> v{} 파괴적 변경: {d}",
pair[0].version, pair[1].version
);
warn_count += 1;
}
for w in &plan.warnings {
eprintln!("경고: v{} -> v{}: {w}", pair[0].version, pair[1].version);
warn_count += 1;
}
}
eprintln!(
"스냅샷 {}개 확인: v{}..v{} ({db})",
files.len(),
files.first().map(|(v, _)| *v).unwrap_or(0),
files.last().map(|(v, _)| *v).unwrap_or(0)
);
if strict && warn_count > 0 {
eprintln!("--strict: 경고 {warn_count}건 — 실패로 처리합니다");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn cmd_check(a: &str, b: &str) -> ExitCode {
let (a, b) = match (load(a), load(b)) {
(Ok(x), Ok(y)) => (x, y),
(Err(c), _) | (_, Err(c)) => return c,
};
if a.hash() == b.hash() {
eprintln!("일치");
ExitCode::SUCCESS
} else {
eprintln!("불일치 — 스냅샷 재생성 또는 마이그레이션 필요");
ExitCode::FAILURE
}
}