1use std::fmt;
2
3use crate::{
4 CompiledStatement, DataModelError, DataRuntime, DataValue, MigrationId, quote_identifier,
5 require_non_empty,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum MigrationOwner {
10 Core,
11 Module(String),
12 CustomerApp(String),
13 AuthPackage(String),
14}
15
16impl fmt::Display for MigrationOwner {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Core => f.write_str("core"),
20 Self::Module(module) => write!(f, "module:{module}"),
21 Self::CustomerApp(app) => write!(f, "customer_app:{app}"),
22 Self::AuthPackage(package) => write!(f, "auth_package:{package}"),
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct MigrationStep {
29 pub id: MigrationId,
30 pub owner: MigrationOwner,
31 pub order: u32,
32 pub description: String,
33 pub online_safe: bool,
34 pub statements: Vec<String>,
35}
36
37impl MigrationStep {
38 pub fn new(
39 id: MigrationId,
40 owner: MigrationOwner,
41 order: u32,
42 description: impl Into<String>,
43 ) -> Result<Self, DataModelError> {
44 Ok(Self {
45 id,
46 owner,
47 order,
48 description: require_non_empty("migration_description", description.into())?,
49 online_safe: true,
50 statements: Vec::new(),
51 })
52 }
53
54 pub fn blocking(mut self) -> Self {
55 self.online_safe = false;
56 self
57 }
58
59 pub fn with_statement(mut self, sql: impl Into<String>) -> Result<Self, DataModelError> {
60 self.statements
61 .push(require_non_empty("migration_statement", sql.into())?);
62 Ok(self)
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Default)]
67pub struct MigrationPlan {
68 steps: Vec<MigrationStep>,
69}
70
71impl MigrationPlan {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn insert(&mut self, step: MigrationStep) -> Result<(), DataModelError> {
77 if self
78 .steps
79 .iter()
80 .any(|existing| existing.owner == step.owner && existing.id == step.id)
81 {
82 return Err(DataModelError::DuplicateMigration {
83 owner: step.owner.to_string(),
84 migration_id: step.id.to_string(),
85 });
86 }
87
88 self.steps.push(step);
89 self.steps.sort_by(|left, right| {
90 owner_rank(&left.owner)
91 .cmp(&owner_rank(&right.owner))
92 .then(left.order.cmp(&right.order))
93 .then(left.id.as_str().cmp(right.id.as_str()))
94 });
95 Ok(())
96 }
97
98 pub fn ordered_steps(&self) -> &[MigrationStep] {
99 &self.steps
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Default)]
104pub struct MigrationRegistry {
105 plan: MigrationPlan,
106}
107
108impl MigrationRegistry {
109 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn register(&mut self, plan: &MigrationPlan) -> Result<(), DataModelError> {
114 for step in plan.ordered_steps() {
115 self.plan.insert(step.clone())?;
116 }
117 Ok(())
118 }
119
120 pub fn composed_plan(&self) -> &MigrationPlan {
121 &self.plan
122 }
123
124 pub fn compile_apply_batch(
125 &self,
126 runtime: &DataRuntime,
127 ) -> Result<CompiledMigrationBatch, DataModelError> {
128 let mut statements = Vec::new();
129 let migrations_table =
130 quote_identifier(&format!("{}.{}", runtime.schema, runtime.migrations_table));
131 statements.push(CompiledStatement {
132 sql: format!(
133 "CREATE TABLE IF NOT EXISTS {migrations_table} (owner TEXT NOT NULL, migration_id TEXT NOT NULL, description TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (owner, migration_id))"
134 ),
135 bind_values: Vec::new(),
136 });
137
138 for step in self.plan.ordered_steps() {
139 if step.statements.is_empty() {
140 return Err(DataModelError::MissingMigrationStatements {
141 migration_id: step.id.to_string(),
142 });
143 }
144
145 for sql in &step.statements {
146 statements.push(CompiledStatement {
147 sql: sql.clone(),
148 bind_values: Vec::new(),
149 });
150 }
151
152 statements.push(CompiledStatement {
153 sql: format!(
154 "INSERT INTO {migrations_table} (owner, migration_id, description) VALUES ($1, $2, $3) ON CONFLICT (owner, migration_id) DO NOTHING"
155 ),
156 bind_values: vec![
157 DataValue::String(step.owner.to_string()),
158 DataValue::String(step.id.to_string()),
159 DataValue::String(step.description.clone()),
160 ],
161 });
162 }
163
164 Ok(CompiledMigrationBatch { statements })
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct CompiledMigrationBatch {
170 pub statements: Vec<CompiledStatement>,
171}
172
173fn owner_rank(owner: &MigrationOwner) -> u8 {
174 match owner {
175 MigrationOwner::Core => 0,
176 MigrationOwner::Module(_) => 1,
177 MigrationOwner::AuthPackage(_) => 2,
178 MigrationOwner::CustomerApp(_) => 3,
179 }
180}