Skip to main content

systemprompt_database/
squash_baseline.rs

1//! Baseline-file placement for squashed extension migrations.
2//!
3//! [`SquashBaselineService`] owns where a squashed baseline lands on disk: it
4//! locates an extension's source crate in the workspace layout
5//! (`crates/{layer}/{extension_id}`, searching upward from a start directory
6//! for the repository root) and writes the baseline SQL produced by
7//! [`crate::lifecycle::MigrationService::squash_through`] into the crate's
8//! `schema/migrations/` directory. Purely filesystem-facing — no SQL is
9//! executed here — so it carries its own [`SquashBaselineError`] instead of
10//! [`crate::RepositoryError`].
11
12use std::path::{Path, PathBuf};
13
14use thiserror::Error;
15
16const CRATE_LAYERS: [&str; 5] = ["domain", "infra", "app", "shared", "entry"];
17
18#[derive(Debug, Error)]
19pub enum SquashBaselineError {
20    #[error(
21        "Could not locate source crate for extension '{extension_id}'. Tried: {tried:?}. The \
22         squash tool maps extension id → crate dir as crates/{{layer}}/{{id}}; if your extension \
23         lives elsewhere, write the baseline file by hand."
24    )]
25    ExtensionCrateNotFound {
26        extension_id: String,
27        tried: Vec<String>,
28    },
29
30    #[error("Failed to create directory {}", path.display())]
31    CreateDir {
32        path: PathBuf,
33        #[source]
34        source: std::io::Error,
35    },
36
37    #[error("Failed to write baseline SQL to {}", path.display())]
38    WriteBaseline {
39        path: PathBuf,
40        #[source]
41        source: std::io::Error,
42    },
43}
44
45#[derive(Debug, Clone, Copy)]
46pub struct SquashBaselineService;
47
48impl SquashBaselineService {
49    pub fn baseline_target_path(
50        start_dir: &Path,
51        extension_id: &str,
52        through: u32,
53    ) -> Result<PathBuf, SquashBaselineError> {
54        let crate_dir = locate_extension_crate(start_dir, extension_id)?;
55        Ok(crate_dir
56            .join("schema")
57            .join("migrations")
58            .join(format!("000_baseline_v{through}.sql")))
59    }
60
61    pub fn write_baseline_file(path: &Path, sql: &str) -> Result<(), SquashBaselineError> {
62        if let Some(parent) = path.parent() {
63            std::fs::create_dir_all(parent).map_err(|source| SquashBaselineError::CreateDir {
64                path: parent.to_path_buf(),
65                source,
66            })?;
67        }
68        std::fs::write(path, sql).map_err(|source| SquashBaselineError::WriteBaseline {
69            path: path.to_path_buf(),
70            source,
71        })
72    }
73}
74
75fn locate_extension_crate(
76    start_dir: &Path,
77    extension_id: &str,
78) -> Result<PathBuf, SquashBaselineError> {
79    let repo_root = find_repo_root(start_dir).unwrap_or_else(|| start_dir.to_path_buf());
80
81    let mut tried = Vec::new();
82    for layer in CRATE_LAYERS {
83        let candidate = repo_root.join("crates").join(layer).join(extension_id);
84        if candidate.join("Cargo.toml").is_file() {
85            return Ok(candidate);
86        }
87        tried.push(candidate.display().to_string());
88    }
89
90    Err(SquashBaselineError::ExtensionCrateNotFound {
91        extension_id: extension_id.to_owned(),
92        tried,
93    })
94}
95
96fn find_repo_root(start: &Path) -> Option<PathBuf> {
97    let mut cur = start;
98    loop {
99        if cur.join("Cargo.toml").is_file() && cur.join("crates").is_dir() {
100            return Some(cur.to_path_buf());
101        }
102        cur = cur.parent()?;
103    }
104}