systemprompt_database/lifecycle/installation/extension/
current.rs1use systemprompt_extension::{ExtensionRegistry, LoaderError};
27use systemprompt_identifiers::ExtensionId;
28
29use super::super::prepare::prepare_extension_schema;
30use crate::lifecycle::migrations::{
31 ChecksumDrift, MigrationService, OrphanedMigration, PendingMigration,
32};
33use crate::services::DatabaseProvider;
34
35#[derive(Debug, Default)]
40pub struct SchemaCurrency {
41 pub fresh_extensions: Vec<ExtensionId>,
42 pub pending: Vec<PendingMigration>,
43 pub drift: Vec<ChecksumDrift>,
44 pub ahead: Vec<OrphanedMigration>,
45 pub superseded_drift: Vec<ChecksumDrift>,
46 pub breaking: Vec<OrphanedMigration>,
47}
48
49impl SchemaCurrency {
50 #[must_use]
51 pub const fn is_current(&self) -> bool {
52 self.fresh_extensions.is_empty() && self.pending.is_empty() && self.drift.is_empty()
53 }
54
55 #[must_use]
56 pub const fn is_ahead(&self) -> bool {
57 !self.ahead.is_empty()
58 }
59
60 #[must_use]
61 pub const fn breaks_this_release(&self) -> bool {
62 !self.breaking.is_empty()
63 }
64}
65
66pub async fn schema_currency(
67 db: &dyn DatabaseProvider,
68 registry: &ExtensionRegistry,
69 disabled_extensions: &[ExtensionId],
70) -> Result<SchemaCurrency, LoaderError> {
71 let migrations = MigrationService::new(db);
72 let mut currency = SchemaCurrency::default();
73
74 for ext in registry.enabled_schema_extensions(disabled_extensions)? {
75 let prepared = prepare_extension_schema(ext.as_ref())?;
76 let freshness = migrations
77 .assess_freshness(&prepared.extension_id, &prepared.owned_tables)
78 .await?;
79 if freshness.tables_total > 0 && freshness.tables_present == 0 {
80 currency.fresh_extensions.push(prepared.extension_id);
81 continue;
82 }
83 if ext.has_migrations() {
84 let latest = ext.migrations().iter().map(|m| m.version).max();
85 let status = migrations.status(ext.as_ref()).await?;
86 let ahead: Vec<OrphanedMigration> = status
87 .orphaned
88 .into_iter()
89 .filter(|m| latest.is_some_and(|latest| m.version > latest))
90 .collect();
91 currency.pending.extend(status.pending);
92 if ahead.is_empty() {
93 currency.drift.extend(status.drift);
94 } else {
95 let breaking = migrations.breaking_versions(&prepared.extension_id).await?;
96 currency.breaking.extend(
97 ahead
98 .iter()
99 .filter(|m| breaking.contains(&m.version))
100 .cloned(),
101 );
102 currency.superseded_drift.extend(status.drift);
103 currency.ahead.extend(ahead);
104 }
105 }
106 }
107
108 Ok(currency)
109}