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//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use std::path::{Path, PathBuf};
16
17use thiserror::Error;
18
19const CRATE_LAYERS: [&str; 5] = ["domain", "infra", "app", "shared", "entry"];
20
21#[derive(Debug, Error)]
22pub enum SquashBaselineError {
23    #[error(
24        "Could not locate source crate for extension '{extension_id}'. Tried: {tried:?}. The \
25         squash tool maps extension id → crate dir as crates/{{layer}}/{{id}}; if your extension \
26         lives elsewhere, write the baseline file by hand."
27    )]
28    ExtensionCrateNotFound {
29        extension_id: String,
30        tried: Vec<String>,
31    },
32
33    #[error("Failed to create directory {}", path.display())]
34    CreateDir {
35        path: PathBuf,
36        #[source]
37        source: std::io::Error,
38    },
39
40    #[error("Failed to write baseline SQL to {}", path.display())]
41    WriteBaseline {
42        path: PathBuf,
43        #[source]
44        source: std::io::Error,
45    },
46}
47
48#[derive(Debug, Clone, Copy)]
49pub struct SquashBaselineService;
50
51impl SquashBaselineService {
52    pub fn baseline_target_path(
53        start_dir: &Path,
54        extension_id: &str,
55        through: u32,
56    ) -> Result<PathBuf, SquashBaselineError> {
57        let crate_dir = locate_extension_crate(start_dir, extension_id)?;
58        Ok(crate_dir
59            .join("schema")
60            .join("migrations")
61            .join(format!("000_baseline_v{through}.sql")))
62    }
63
64    pub fn write_baseline_file(path: &Path, sql: &str) -> Result<(), SquashBaselineError> {
65        if let Some(parent) = path.parent() {
66            std::fs::create_dir_all(parent).map_err(|source| SquashBaselineError::CreateDir {
67                path: parent.to_path_buf(),
68                source,
69            })?;
70        }
71        std::fs::write(path, sql).map_err(|source| SquashBaselineError::WriteBaseline {
72            path: path.to_path_buf(),
73            source,
74        })
75    }
76}
77
78fn locate_extension_crate(
79    start_dir: &Path,
80    extension_id: &str,
81) -> Result<PathBuf, SquashBaselineError> {
82    let repo_root = find_repo_root(start_dir).unwrap_or_else(|| start_dir.to_path_buf());
83
84    let mut tried = Vec::new();
85    for layer in CRATE_LAYERS {
86        let candidate = repo_root.join("crates").join(layer).join(extension_id);
87        if candidate.join("Cargo.toml").is_file() {
88            return Ok(candidate);
89        }
90        tried.push(candidate.display().to_string());
91    }
92
93    Err(SquashBaselineError::ExtensionCrateNotFound {
94        extension_id: extension_id.to_owned(),
95        tried,
96    })
97}
98
99fn find_repo_root(start: &Path) -> Option<PathBuf> {
100    let mut cur = start;
101    loop {
102        if cur.join("Cargo.toml").is_file() && cur.join("crates").is_dir() {
103            return Some(cur.to_path_buf());
104        }
105        cur = cur.parent()?;
106    }
107}