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