systemprompt_database/lifecycle/migrations/
stamp.rs1use super::MigrationService;
17use systemprompt_extension::{Extension, LoaderError};
18use tracing::warn;
19
20#[derive(Debug, Clone)]
23pub struct BaselineStamp {
24 pub id: String,
25 pub version: u32,
26 pub name: String,
27 pub checksum: String,
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct FreshnessCheck {
32 pub no_history: bool,
33 pub tables_present: usize,
34 pub tables_total: usize,
35}
36
37impl FreshnessCheck {
38 #[must_use]
39 pub const fn is_fresh(&self) -> bool {
40 self.no_history && self.tables_present == 0
41 }
42}
43
44impl MigrationService<'_> {
45 pub async fn assess_freshness(
46 &self,
47 extension_id: &str,
48 owned_tables: &[String],
49 ) -> Result<FreshnessCheck, LoaderError> {
50 self.ensure_migrations_table_exists().await?;
51
52 let no_history = self.get_applied_migrations(extension_id).await?.is_empty();
53
54 let mut tables_present = 0usize;
55 for table in owned_tables {
56 let (schema, name) = table.split_once('.').unwrap_or(("public", table.as_str()));
57 let result = self
58 .db
59 .query_raw_with(
60 &"SELECT 1 AS present FROM information_schema.tables WHERE table_schema = $1 \
61 AND table_name = $2",
62 &[&schema, &name],
63 )
64 .await
65 .map_err(|e| LoaderError::MigrationFailed {
66 extension: extension_id.to_owned(),
67 message: format!("Failed to check for existing table '{table}': {e}"),
68 })?;
69 if !result.rows.is_empty() {
70 tables_present += 1;
71 }
72 }
73
74 let check = FreshnessCheck {
75 no_history,
76 tables_present,
77 tables_total: owned_tables.len(),
78 };
79
80 if check.no_history && check.tables_present > 0 && check.tables_present < check.tables_total
81 {
82 warn!(
83 extension = %extension_id,
84 tables_present = check.tables_present,
85 tables_total = check.tables_total,
86 "Extension has no migration history but some owned tables already exist; \
87 treating as an established database and executing migrations normally"
88 );
89 }
90
91 Ok(check)
92 }
93
94 #[must_use]
95 pub fn baseline_stamp_rows(extension: &dyn Extension) -> Vec<BaselineStamp> {
96 let ext_id = extension.metadata().id;
97 extension
98 .migrations()
99 .iter()
100 .filter(|migration| !migration.tombstone)
101 .map(|migration| BaselineStamp {
102 id: format!("{}_{:03}", ext_id, migration.version),
103 version: migration.version,
104 name: migration.name.clone(),
105 checksum: migration.checksum(),
106 })
107 .collect()
108 }
109}