type_bridge_migration/state/
schema.rs1use std::collections::BTreeMap;
9use std::sync::LazyLock;
10
11use serde::{Deserialize, Serialize};
12use type_bridge_orm::schema::SchemaInfo;
13use type_bridge_orm::schema::info::{AttributeSchemaEntry, EntitySchemaEntry, OwnedAttributeEntry};
14use type_bridge_orm::{Annotation, ValueType};
15
16pub(crate) mod labels {
21 use type_bridge_contract::reserved::{
22 LEGACY_LEDGER_APP_LABEL, LEGACY_LEDGER_APPLIED_AT, LEGACY_LEDGER_APPLIED_ENTITY,
23 LEGACY_LEDGER_CHECKSUM, LEGACY_LEDGER_MIGRATION_ID, LEGACY_LEDGER_NAME,
24 };
25
26 pub const APPLIED_ENTITY: &str = LEGACY_LEDGER_APPLIED_ENTITY;
28 pub const RUN_ENTITY: &str = "type_bridge_migration_run";
30
31 pub const MIGRATION_ID: &str = LEGACY_LEDGER_MIGRATION_ID;
33 pub const APP_LABEL: &str = LEGACY_LEDGER_APP_LABEL;
35 pub const NAME: &str = LEGACY_LEDGER_NAME;
37 pub const APPLIED_AT: &str = LEGACY_LEDGER_APPLIED_AT;
39 pub const CHECKSUM: &str = LEGACY_LEDGER_CHECKSUM;
41 pub const RUN_ID: &str = "migration_run_id";
43 pub const DIRECTION: &str = "migration_direction";
45 pub const STATUS: &str = "migration_status";
47 pub const STARTED_AT: &str = "migration_started_at";
49 pub const FINISHED_AT: &str = "migration_finished_at";
51 pub const ERROR: &str = "migration_error";
53 pub const EXECUTOR_IP: &str = "migration_executor_ip";
55 pub const EXECUTOR_MAC: &str = "migration_executor_mac";
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum MigrationStateSchemaKind {
63 Entity,
65 Relation,
67 Attribute,
69 Role,
71}
72
73static MIGRATION_STATE_SCHEMA: LazyLock<SchemaInfo> = LazyLock::new(build_migration_state_schema);
74
75pub fn migration_state_schema() -> &'static SchemaInfo {
82 &MIGRATION_STATE_SCHEMA
83}
84
85pub fn applied_migration_entity_label() -> &'static str {
91 labels::APPLIED_ENTITY
92}
93
94pub fn is_migration_state_type(kind: MigrationStateSchemaKind, label: &str) -> bool {
99 let schema = migration_state_schema();
100 match kind {
101 MigrationStateSchemaKind::Entity => schema.entities.contains_key(label),
102 MigrationStateSchemaKind::Relation => schema.relations.contains_key(label),
103 MigrationStateSchemaKind::Attribute => schema.attributes.contains_key(label),
104 MigrationStateSchemaKind::Role => {
105 let Some((relation_label, role_label)) = label.split_once(':') else {
106 return false;
107 };
108 schema
109 .relations
110 .get(relation_label)
111 .is_some_and(|relation| {
112 relation
113 .roles
114 .iter()
115 .any(|role| role.role_name == role_label)
116 })
117 }
118 }
119}
120
121fn build_migration_state_schema() -> SchemaInfo {
122 use labels::*;
123
124 let attribute_specs = [
125 (MIGRATION_ID, ValueType::String),
126 (APP_LABEL, ValueType::String),
127 (NAME, ValueType::String),
128 (APPLIED_AT, ValueType::DateTime),
129 (CHECKSUM, ValueType::String),
130 (RUN_ID, ValueType::String),
131 (DIRECTION, ValueType::String),
132 (STATUS, ValueType::String),
133 (STARTED_AT, ValueType::DateTime),
134 (FINISHED_AT, ValueType::DateTime),
135 (ERROR, ValueType::String),
136 (EXECUTOR_IP, ValueType::String),
137 (EXECUTOR_MAC, ValueType::String),
138 ];
139
140 let attributes = attribute_specs
141 .into_iter()
142 .map(|(label, value_type)| {
143 (
144 label.to_string(),
145 AttributeSchemaEntry::new(label, value_type),
146 )
147 })
148 .collect();
149
150 let entities = [
151 (
152 APPLIED_ENTITY,
153 vec![
154 owned_attribute(MIGRATION_ID, ValueType::String, true),
155 owned_attribute(APP_LABEL, ValueType::String, false),
156 owned_attribute(NAME, ValueType::String, false),
157 owned_attribute(APPLIED_AT, ValueType::DateTime, false),
158 owned_attribute(CHECKSUM, ValueType::String, false),
159 ],
160 ),
161 (
162 RUN_ENTITY,
163 vec![
164 owned_attribute(RUN_ID, ValueType::String, true),
165 owned_attribute(APP_LABEL, ValueType::String, false),
166 owned_attribute(NAME, ValueType::String, false),
167 owned_attribute(CHECKSUM, ValueType::String, false),
168 owned_attribute(DIRECTION, ValueType::String, false),
169 owned_attribute(STATUS, ValueType::String, false),
170 owned_attribute(STARTED_AT, ValueType::DateTime, false),
171 owned_attribute(FINISHED_AT, ValueType::DateTime, false),
172 owned_attribute(ERROR, ValueType::String, false),
173 owned_attribute(EXECUTOR_IP, ValueType::String, false),
174 owned_attribute(EXECUTOR_MAC, ValueType::String, false),
175 ],
176 ),
177 ]
178 .into_iter()
179 .map(|(label, owned_attributes)| {
180 (
181 label.to_string(),
182 EntitySchemaEntry {
183 type_name: label.to_string(),
184 is_abstract: false,
185 parent_type: None,
186 owned_attributes,
187 plays_cardinalities: BTreeMap::new(),
188 doc: None,
189 meta: Default::default(),
190 },
191 )
192 })
193 .collect();
194
195 SchemaInfo {
196 entities,
197 relations: BTreeMap::new(),
198 attributes,
199 }
200}
201
202fn owned_attribute(label: &str, value_type: ValueType, is_key: bool) -> OwnedAttributeEntry {
203 OwnedAttributeEntry {
204 attr_name: label.to_string(),
205 value_type,
206 annotations: if is_key {
207 vec![Annotation::Key]
208 } else {
209 Vec::new()
210 },
211 is_ordered: false,
212 doc: None,
213 meta: Default::default(),
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use std::collections::BTreeSet;
221
222 #[test]
223 fn descriptor_contains_the_complete_current_state_schema() {
224 let schema = migration_state_schema();
225
226 assert_eq!(
227 schema
228 .entities
229 .keys()
230 .map(String::as_str)
231 .collect::<BTreeSet<_>>(),
232 BTreeSet::from([labels::APPLIED_ENTITY, labels::RUN_ENTITY])
233 );
234 assert_eq!(
235 schema
236 .attributes
237 .keys()
238 .map(String::as_str)
239 .collect::<BTreeSet<_>>(),
240 BTreeSet::from([
241 labels::MIGRATION_ID,
242 labels::APP_LABEL,
243 labels::NAME,
244 labels::APPLIED_AT,
245 labels::CHECKSUM,
246 labels::RUN_ID,
247 labels::DIRECTION,
248 labels::STATUS,
249 labels::STARTED_AT,
250 labels::FINISHED_AT,
251 labels::ERROR,
252 labels::EXECUTOR_IP,
253 labels::EXECUTOR_MAC,
254 ])
255 );
256 assert!(schema.relations.is_empty());
257 assert!(
258 schema
259 .entities
260 .contains_key(applied_migration_entity_label())
261 );
262 }
263
264 #[test]
265 fn descriptor_preserves_value_types_and_key_ownerships() {
266 let schema = migration_state_schema();
267
268 assert_eq!(
269 schema.attributes[labels::APPLIED_AT].value_type,
270 ValueType::DateTime
271 );
272 assert_eq!(
273 schema.attributes[labels::STARTED_AT].value_type,
274 ValueType::DateTime
275 );
276 assert_eq!(
277 schema.attributes[labels::FINISHED_AT].value_type,
278 ValueType::DateTime
279 );
280
281 let applied = &schema.entities[labels::APPLIED_ENTITY];
282 let applied_key = applied
283 .owned_attributes
284 .iter()
285 .find(|attribute| attribute.attr_name == labels::MIGRATION_ID)
286 .unwrap();
287 assert_eq!(applied_key.annotations, vec![Annotation::Key]);
288
289 let run = &schema.entities[labels::RUN_ENTITY];
290 let run_key = run
291 .owned_attributes
292 .iter()
293 .find(|attribute| attribute.attr_name == labels::RUN_ID)
294 .unwrap();
295 assert_eq!(run_key.annotations, vec![Annotation::Key]);
296 }
297
298 #[test]
299 fn predicate_is_exact_and_kind_sensitive() {
300 assert!(is_migration_state_type(
301 MigrationStateSchemaKind::Entity,
302 labels::APPLIED_ENTITY
303 ));
304 assert!(is_migration_state_type(
305 MigrationStateSchemaKind::Attribute,
306 labels::CHECKSUM
307 ));
308 assert!(!is_migration_state_type(
309 MigrationStateSchemaKind::Attribute,
310 labels::APPLIED_ENTITY
311 ));
312 assert!(!is_migration_state_type(
313 MigrationStateSchemaKind::Entity,
314 "type_bridge_migration_custom"
315 ));
316 assert!(!is_migration_state_type(
317 MigrationStateSchemaKind::Role,
318 "unqualified-role"
319 ));
320 }
321
322 #[test]
323 fn canonical_schema_generates_the_existing_typeql_shape() {
324 let typeql = migration_state_schema().to_typeql().unwrap();
325
326 assert!(typeql.contains("attribute migration_applied_at, value datetime;"));
327 assert!(typeql.contains("attribute migration_started_at, value datetime;"));
328 assert!(typeql.contains("entity type_bridge_migration,"));
329 assert!(typeql.contains("owns migration_id @key"));
330 assert!(typeql.contains("entity type_bridge_migration_run,"));
331 assert!(typeql.contains("owns migration_run_id @key"));
332 }
333}