This commit is contained in:
Kelorastus
2026-09-16 11:42:24 +02:00
parent b7f3061be3
commit 3fdfde7043
18 changed files with 1485 additions and 21 deletions
+45 -10
View File
@@ -107,6 +107,11 @@ impl Pg {
if self.running() {
return Ok(());
}
// Another install's Postgres on the shared port would leave only "exit code 1" in the log.
if crate::stack::port_taken(PG_PORT) {
return Err("Another ODIAN install is already running on this PC: it holds the database port. Stop it in the launcher window that started it, then try again."
.to_string());
}
let mut cmd = self.tool("pg_ctl");
cmd
.arg("start")
@@ -151,6 +156,34 @@ impl Pg {
Self::run(cmd, "psql")
}
/// Run a script on the `odian` database through psql's stdin and return what it printed
/// (unaligned, tuples only). For sync, whose JSON payloads are too large for a command line.
pub fn script(&self, sql: &str) -> Result<String, String> {
use std::io::Write;
let port = PG_PORT.to_string();
let mut cmd = self.tool("psql");
cmd
.env("PGCLIENTENCODING", "UTF8")
.args(["-X", "-q", "-A", "-t", "-v", "ON_ERROR_STOP=1", "-h", "127.0.0.1", "-p", port.as_str()])
.args(["-U", PG_USER, "-d", PG_DATABASE, "-f", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| format!("cannot run psql: {e}"))?;
let mut stdin = child.stdin.take().ok_or("cannot write to psql")?;
// Feed from a thread: psql filling its stdout pipe while we still write would deadlock.
let body = sql.as_bytes().to_vec();
let writer = std::thread::spawn(move || stdin.write_all(&body));
let out = child.wait_with_output().map_err(|e| format!("psql failed: {e}"))?;
let _ = writer.join();
if out.status.success() {
return Ok(String::from_utf8_lossy(&out.stdout).into_owned());
}
let stderr = String::from_utf8_lossy(&out.stderr);
let tail: String = stderr.chars().rev().take(1500).collect::<Vec<_>>().into_iter().rev().collect();
Err(format!("psql failed ({}): {}", out.status, tail.trim()))
}
/// Replace the `odian` database with a snapshot. Only for a PC whose restore never
/// completed: anything an operator did in the old database is dropped.
pub fn restore(
@@ -225,24 +258,26 @@ impl Pg {
.args(["-h", "127.0.0.1", "-p", port.as_str(), "-U", PG_USER, "-d", PG_DATABASE])
.args(["--no-owner", "--no-privileges", "--exit-on-error"]);
if pre_data {
cmd.arg("-L").arg(self.pre_data_list(&dump)?);
// A fresh database already has schema public; --exit-on-error would stop on the duplicate.
let drop = |line: &str| line.contains(" SCHEMA - public ") || line.contains(" COMMENT - SCHEMA public ");
cmd.arg("-L").arg(self.toc_without(&dump, "pre-data.list", &drop)?);
} else if file.section == "post-data" {
// A DHP running sync has marking triggers on its tables; they call sync.mark(), which
// this database gets only when the launcher loads the version's sync schema.
let drop = |line: &str| line.contains(" TRIGGER ") && line.contains(" sync_mark_");
cmd.arg("-L").arg(self.toc_without(&dump, "post-data.list", &drop)?);
}
cmd.arg(&dump);
Self::run(cmd, &format!("pg_restore {}", file.path)).map(|_| ())
}
/// The pre-data table of contents without `CREATE SCHEMA public`: a fresh database
/// already has that schema, and --exit-on-error would stop on the duplicate.
fn pre_data_list(&self, dump: &Path) -> Result<PathBuf, String> {
/// The dump's table of contents without the entries `drop` names, for `pg_restore -L`.
fn toc_without(&self, dump: &Path, list_name: &str, drop: &dyn Fn(&str) -> bool) -> Result<PathBuf, String> {
let mut cmd = self.tool("pg_restore");
cmd.arg("-l").arg(dump);
let listing = Self::run(cmd, "pg_restore -l")?;
let kept: String = listing
.lines()
.filter(|line| !(line.contains(" SCHEMA - public ") || line.contains(" COMMENT - SCHEMA public ")))
.map(|line| format!("{line}\n"))
.collect();
let path = self.logs.join("pre-data.list");
let kept: String = listing.lines().filter(|line| !drop(line)).map(|line| format!("{line}\n")).collect();
let path = self.logs.join(list_name);
std::fs::write(&path, kept).map_err(|e| format!("cannot write {}: {e}", path.display()))?;
Ok(path)
}