Skip to main content

systemprompt_database/lifecycle/installation/
report.rs

1//! Outcome of a schema installation that succeeded but left typed drift
2//! behind: a declared foreign key an established database could not create.
3//! Callers surface the report — health endpoints, `infra db migrate` — rather
4//! than reading it from the log.
5//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use serde::Serialize;
10use systemprompt_identifiers::ExtensionId;
11
12use super::undeclared::SchemaResidue;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct ForeignKeyDrift {
16    pub extension: ExtensionId,
17    pub table: String,
18    pub constraint: String,
19    pub sql: String,
20    pub cause: String,
21}
22
23/// What `install_extension_schemas*` applied and what it could not.
24///
25/// `foreign_key_drift` is non-empty only for an established extension whose
26/// declarative foreign key cannot be created; the install still committed
27/// every other statement. A fresh database never reports drift — the same
28/// condition fails the install there. `residue` is what the database holds
29/// that no registered extension declares (tables and migration ledgers a
30/// deleted crate left behind); it never fails an install and is reported so
31/// a drop migration can be written.
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
33pub struct SchemaInstallReport {
34    pub foreign_key_drift: Vec<ForeignKeyDrift>,
35    pub residue: SchemaResidue,
36    pub ahead: Vec<SchemaAhead>,
37}
38
39impl SchemaAhead {
40    #[must_use]
41    pub fn from_currency(currency: &super::extension::current::SchemaCurrency) -> Vec<Self> {
42        currency
43            .ahead
44            .iter()
45            .map(|m| Self {
46                extension: m.extension_id.clone(),
47                version: m.version,
48                name: m.name.clone(),
49                breaks_older: currency
50                    .breaking
51                    .iter()
52                    .any(|b| b.extension_id == m.extension_id && b.version == m.version),
53            })
54            .collect()
55    }
56}
57
58/// A ledger row a newer release wrote: the database is ahead of this binary.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
60pub struct SchemaAhead {
61    pub extension: ExtensionId,
62    pub version: u32,
63    pub name: String,
64    pub breaks_older: bool,
65}
66
67impl SchemaInstallReport {
68    #[must_use]
69    pub const fn is_clean(&self) -> bool {
70        self.foreign_key_drift.is_empty()
71    }
72
73    #[must_use]
74    pub const fn is_tidy(&self) -> bool {
75        self.residue.is_empty()
76    }
77
78    #[must_use]
79    pub const fn is_ahead(&self) -> bool {
80        !self.ahead.is_empty()
81    }
82
83    #[must_use]
84    pub fn breaks_this_release(&self) -> bool {
85        self.ahead.iter().any(|m| m.breaks_older)
86    }
87}