Skip to main content

systemprompt_database/lifecycle/installation/extension/
current.rs

1//! Read-only check that the database schema matches the binary.
2//!
3//! A replica that boots without migrating (`--skip-migrate`, or
4//! `database.migrate_on_boot: false`) must still refuse a database the
5//! migration step never reached. [`schema_currency`] reports every enabled
6//! extension whose owned tables are all absent (never installed), every
7//! defined migration with no ledger row (pending), and every applied
8//! migration whose checksum no longer matches its file (drift). It reads the
9//! same primitives the installer uses and executes no extension DDL.
10//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use systemprompt_extension::{ExtensionRegistry, LoaderError};
15use systemprompt_identifiers::ExtensionId;
16
17use super::super::prepare::prepare_extension_schema;
18use crate::lifecycle::migrations::{ChecksumDrift, MigrationService, PendingMigration};
19use crate::services::DatabaseProvider;
20
21/// How far the live schema is behind the binary's extensions.
22#[derive(Debug, Default)]
23pub struct SchemaCurrency {
24    pub fresh_extensions: Vec<ExtensionId>,
25    pub pending: Vec<PendingMigration>,
26    pub drift: Vec<ChecksumDrift>,
27}
28
29impl SchemaCurrency {
30    #[must_use]
31    pub const fn is_current(&self) -> bool {
32        self.fresh_extensions.is_empty() && self.pending.is_empty() && self.drift.is_empty()
33    }
34}
35
36pub async fn schema_currency(
37    db: &dyn DatabaseProvider,
38    registry: &ExtensionRegistry,
39    disabled_extensions: &[ExtensionId],
40) -> Result<SchemaCurrency, LoaderError> {
41    let migrations = MigrationService::new(db);
42    let mut currency = SchemaCurrency::default();
43
44    for ext in registry.enabled_schema_extensions(disabled_extensions)? {
45        let prepared = prepare_extension_schema(ext.as_ref())?;
46        let freshness = migrations
47            .assess_freshness(&prepared.extension_id, &prepared.owned_tables)
48            .await?;
49        if freshness.tables_total > 0 && freshness.tables_present == 0 {
50            currency.fresh_extensions.push(prepared.extension_id);
51            continue;
52        }
53        if ext.has_migrations() {
54            let status = migrations.status(ext.as_ref()).await?;
55            currency.pending.extend(status.pending);
56            currency.drift.extend(status.drift);
57        }
58    }
59
60    Ok(currency)
61}