Skip to main content

alembic_engine/
types.rs

1//! core engine types and adapter contract.
2
3use alembic_core::{key_string, JsonMap, Key, Object, Schema, TypeName, Uid};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::fmt;
8
9/// generic backend identifier (integer or string/uuid).
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum BackendId {
13    Int(u64),
14    String(String),
15}
16
17impl fmt::Display for BackendId {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            BackendId::Int(id) => write!(f, "{}", id),
21            BackendId::String(id) => write!(f, "{}", id),
22        }
23    }
24}
25
26impl From<u64> for BackendId {
27    fn from(id: u64) -> Self {
28        BackendId::Int(id)
29    }
30}
31
32impl From<String> for BackendId {
33    fn from(id: String) -> Self {
34        BackendId::String(id)
35    }
36}
37
38/// field-level change for an update op.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct FieldChange {
41    /// field name within attrs.
42    pub field: String,
43    /// previous value from observed state.
44    pub from: serde_json::Value,
45    /// desired value from the ir.
46    pub to: serde_json::Value,
47}
48
49/// plan operation.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(tag = "op", rename_all = "snake_case")]
52pub enum Op {
53    /// create a new backend object.
54    Create {
55        uid: Uid,
56        type_name: TypeName,
57        desired: Object,
58    },
59    /// update an existing backend object.
60    Update {
61        uid: Uid,
62        type_name: TypeName,
63        desired: Object,
64        changes: Vec<FieldChange>,
65        #[serde(skip_serializing_if = "Option::is_none")]
66        backend_id: Option<BackendId>,
67    },
68    /// delete a backend object.
69    Delete {
70        uid: Uid,
71        type_name: TypeName,
72        key: Key,
73        #[serde(skip_serializing_if = "Option::is_none")]
74        backend_id: Option<BackendId>,
75    },
76}
77
78impl Op {
79    /// returns the ir uid for this operation.
80    pub fn uid(&self) -> Uid {
81        match self {
82            Op::Create { uid, .. } => *uid,
83            Op::Update { uid, .. } => *uid,
84            Op::Delete { uid, .. } => *uid,
85        }
86    }
87
88    /// returns the type name for this operation.
89    pub fn type_name(&self) -> &TypeName {
90        match self {
91            Op::Create { type_name, .. } => type_name,
92            Op::Update { type_name, .. } => type_name,
93            Op::Delete { type_name, .. } => type_name,
94        }
95    }
96}
97
98/// full plan document.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Plan {
101    /// schema definitions required for apply.
102    pub schema: Schema,
103    /// ordered list of operations.
104    pub ops: Vec<Op>,
105    /// high-level summary of the plan.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub summary: Option<PlanSummary>,
108}
109
110/// high-level summary of plan operations.
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct PlanSummary {
113    /// number of objects to create.
114    pub create: usize,
115    /// number of objects to update.
116    pub update: usize,
117    /// number of objects to delete.
118    pub delete: usize,
119}
120
121impl Plan {
122    /// build a summary for the current plan.
123    pub fn summary(&self) -> PlanSummary {
124        let mut summary = PlanSummary::default();
125        for op in &self.ops {
126            match op {
127                Op::Create { .. } => summary.create += 1,
128                Op::Update { .. } => summary.update += 1,
129                Op::Delete { .. } => summary.delete += 1,
130            }
131        }
132        summary
133    }
134}
135
136/// observed backend object representation.
137#[derive(Debug, Clone)]
138pub struct ObservedObject {
139    /// object type.
140    pub type_name: TypeName,
141    /// human key for matching.
142    pub key: Key,
143    /// observed attrs mapped to ir types.
144    pub attrs: JsonMap,
145    /// backend id when known.
146    pub backend_id: Option<BackendId>,
147}
148
149/// observed backend state indexed by id and key.
150#[derive(Debug, Default, Clone)]
151pub struct ObservedState {
152    /// observed objects keyed by backend id.
153    pub by_backend_id: BTreeMap<(TypeName, BackendId), ObservedObject>,
154    /// observed objects keyed by natural key.
155    pub by_key: BTreeMap<(TypeName, String), ObservedObject>,
156}
157
158impl ObservedState {
159    /// insert an observed object into both indexes.
160    pub fn insert(&mut self, object: ObservedObject) {
161        if let Some(id) = &object.backend_id {
162            self.by_backend_id
163                .insert((object.type_name.clone(), id.clone()), object.clone());
164        }
165        self.by_key
166            .insert((object.type_name.clone(), key_string(&object.key)), object);
167    }
168}
169
170/// result for a single applied operation.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct AppliedOp {
173    /// ir uid for the operation.
174    pub uid: Uid,
175    /// type for the operation.
176    pub type_name: TypeName,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    /// backend id returned by the adapter, if any.
179    pub backend_id: Option<BackendId>,
180}
181
182/// aggregated apply report.
183#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184pub struct ApplyReport {
185    /// list of operations applied by the adapter.
186    pub applied: Vec<AppliedOp>,
187    /// schema provisioning report (populated when ensure_schema runs).
188    #[serde(default)]
189    pub provision: ProvisionReport,
190}
191
192/// report from ensure_schema provisioning.
193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
194pub struct ProvisionReport {
195    /// custom fields created on the backend.
196    pub created_fields: Vec<String>,
197    /// tags created on the backend.
198    pub created_tags: Vec<String>,
199    /// custom object types created on the backend.
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub created_object_types: Vec<String>,
202    /// custom object fields created on the backend.
203    #[serde(default, skip_serializing_if = "Vec::is_empty")]
204    pub created_object_fields: Vec<String>,
205    /// object types deprecated on the backend.
206    #[serde(default, skip_serializing_if = "Vec::is_empty")]
207    pub deprecated_object_types: Vec<String>,
208    /// object fields deprecated on the backend.
209    #[serde(default, skip_serializing_if = "Vec::is_empty")]
210    pub deprecated_object_fields: Vec<String>,
211    /// object types deleted on the backend.
212    #[serde(default, skip_serializing_if = "Vec::is_empty")]
213    pub deleted_object_types: Vec<String>,
214    /// object fields deleted on the backend.
215    #[serde(default, skip_serializing_if = "Vec::is_empty")]
216    pub deleted_object_fields: Vec<String>,
217}
218
219impl ProvisionReport {
220    pub fn is_empty(&self) -> bool {
221        self.created_fields.is_empty()
222            && self.created_tags.is_empty()
223            && self.created_object_types.is_empty()
224            && self.created_object_fields.is_empty()
225            && self.deprecated_object_types.is_empty()
226            && self.deprecated_object_fields.is_empty()
227            && self.deleted_object_types.is_empty()
228            && self.deleted_object_fields.is_empty()
229    }
230}
231
232impl fmt::Display for ProvisionReport {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        if self.is_empty() {
235            return write!(f, "no schema changes");
236        }
237
238        let mut first = true;
239        let sections: &[(&str, &[String])] = &[
240            ("fields created", &self.created_fields),
241            ("tags created", &self.created_tags),
242            ("object types created", &self.created_object_types),
243            ("object fields created", &self.created_object_fields),
244            ("object types deprecated", &self.deprecated_object_types),
245            ("object fields deprecated", &self.deprecated_object_fields),
246            ("object types deleted", &self.deleted_object_types),
247            ("object fields deleted", &self.deleted_object_fields),
248        ];
249
250        for (label, items) in sections {
251            if items.is_empty() {
252                continue;
253            }
254            if !first {
255                write!(f, ", ")?;
256            }
257            write!(f, "{} {label}", items.len())?;
258            first = false;
259        }
260
261        Ok(())
262    }
263}
264
265/// adapter contract for backend-specific io.
266#[async_trait]
267pub trait Adapter: Send + Sync {
268    async fn read(
269        &self,
270        schema: &Schema,
271        types: &[TypeName],
272        state: &crate::state::StateStore,
273    ) -> anyhow::Result<ObservedState>;
274    async fn write(
275        &self,
276        schema: &Schema,
277        ops: &[Op],
278        state: &crate::state::StateStore,
279    ) -> anyhow::Result<ApplyReport>;
280    async fn ensure_schema(&self, _schema: &Schema) -> anyhow::Result<ProvisionReport> {
281        Ok(ProvisionReport::default())
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use alembic_core::{Key, TypeName, Uid};
289
290    #[test]
291    fn backend_id_serialization() {
292        let int_id = BackendId::Int(123);
293        let json = serde_json::to_string(&int_id).unwrap();
294        assert_eq!(json, "123");
295        let back: BackendId = serde_json::from_str(&json).unwrap();
296        assert_eq!(back, int_id);
297
298        let str_id = BackendId::String("uuid".to_string());
299        let json = serde_json::to_string(&str_id).unwrap();
300        assert_eq!(json, "\"uuid\"");
301        let back: BackendId = serde_json::from_str(&json).unwrap();
302        assert_eq!(back, str_id);
303    }
304
305    #[test]
306    fn op_helpers() {
307        let uid = Uid::from_u128(1);
308        let type_name = TypeName::new("test.type");
309        let op = Op::Delete {
310            uid,
311            type_name: type_name.clone(),
312            key: Key::default(),
313            backend_id: None,
314        };
315        assert_eq!(op.uid(), uid);
316        assert_eq!(op.type_name(), &type_name);
317    }
318}