use std::collections::BTreeMap;
use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
use type_bridge_orm::_entity::Annotation;
use type_bridge_orm::_schema::SchemaInfo;
use type_bridge_orm::_schema::info::{
AttributeSchemaEntry, EntitySchemaEntry, OwnedAttributeEntry,
};
use type_bridge_orm::ValueType;
pub(crate) mod labels {
use type_bridge_contract::reserved::{
LEGACY_LEDGER_APP_LABEL, LEGACY_LEDGER_APPLIED_AT, LEGACY_LEDGER_APPLIED_ENTITY,
LEGACY_LEDGER_CHECKSUM, LEGACY_LEDGER_MIGRATION_ID, LEGACY_LEDGER_NAME,
};
pub const APPLIED_ENTITY: &str = LEGACY_LEDGER_APPLIED_ENTITY;
pub const RUN_ENTITY: &str = "type_bridge_migration_run";
pub const MIGRATION_ID: &str = LEGACY_LEDGER_MIGRATION_ID;
pub const APP_LABEL: &str = LEGACY_LEDGER_APP_LABEL;
pub const NAME: &str = LEGACY_LEDGER_NAME;
pub const APPLIED_AT: &str = LEGACY_LEDGER_APPLIED_AT;
pub const CHECKSUM: &str = LEGACY_LEDGER_CHECKSUM;
pub const RUN_ID: &str = "migration_run_id";
pub const DIRECTION: &str = "migration_direction";
pub const STATUS: &str = "migration_status";
pub const STARTED_AT: &str = "migration_started_at";
pub const FINISHED_AT: &str = "migration_finished_at";
pub const ERROR: &str = "migration_error";
pub const EXECUTOR_IP: &str = "migration_executor_ip";
pub const EXECUTOR_MAC: &str = "migration_executor_mac";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MigrationStateSchemaKind {
Entity,
Relation,
Attribute,
Role,
}
static MIGRATION_STATE_SCHEMA: LazyLock<SchemaInfo> = LazyLock::new(build_migration_state_schema);
pub fn migration_state_schema() -> &'static SchemaInfo {
&MIGRATION_STATE_SCHEMA
}
pub fn applied_migration_entity_label() -> &'static str {
labels::APPLIED_ENTITY
}
pub fn is_migration_state_type(kind: MigrationStateSchemaKind, label: &str) -> bool {
let schema = migration_state_schema();
match kind {
MigrationStateSchemaKind::Entity => schema.entities.contains_key(label),
MigrationStateSchemaKind::Relation => schema.relations.contains_key(label),
MigrationStateSchemaKind::Attribute => schema.attributes.contains_key(label),
MigrationStateSchemaKind::Role => {
let Some((relation_label, role_label)) = label.split_once(':') else {
return false;
};
schema
.relations
.get(relation_label)
.is_some_and(|relation| {
relation
.roles
.iter()
.any(|role| role.role_name == role_label)
})
}
}
}
fn build_migration_state_schema() -> SchemaInfo {
use labels::*;
let attribute_specs = [
(MIGRATION_ID, ValueType::String),
(APP_LABEL, ValueType::String),
(NAME, ValueType::String),
(APPLIED_AT, ValueType::DateTime),
(CHECKSUM, ValueType::String),
(RUN_ID, ValueType::String),
(DIRECTION, ValueType::String),
(STATUS, ValueType::String),
(STARTED_AT, ValueType::DateTime),
(FINISHED_AT, ValueType::DateTime),
(ERROR, ValueType::String),
(EXECUTOR_IP, ValueType::String),
(EXECUTOR_MAC, ValueType::String),
];
let attributes = attribute_specs
.into_iter()
.map(|(label, value_type)| {
(
label.to_string(),
AttributeSchemaEntry::new(label, value_type),
)
})
.collect();
let entities = [
(
APPLIED_ENTITY,
vec![
owned_attribute(MIGRATION_ID, ValueType::String, true),
owned_attribute(APP_LABEL, ValueType::String, false),
owned_attribute(NAME, ValueType::String, false),
owned_attribute(APPLIED_AT, ValueType::DateTime, false),
owned_attribute(CHECKSUM, ValueType::String, false),
],
),
(
RUN_ENTITY,
vec![
owned_attribute(RUN_ID, ValueType::String, true),
owned_attribute(APP_LABEL, ValueType::String, false),
owned_attribute(NAME, ValueType::String, false),
owned_attribute(CHECKSUM, ValueType::String, false),
owned_attribute(DIRECTION, ValueType::String, false),
owned_attribute(STATUS, ValueType::String, false),
owned_attribute(STARTED_AT, ValueType::DateTime, false),
owned_attribute(FINISHED_AT, ValueType::DateTime, false),
owned_attribute(ERROR, ValueType::String, false),
owned_attribute(EXECUTOR_IP, ValueType::String, false),
owned_attribute(EXECUTOR_MAC, ValueType::String, false),
],
),
]
.into_iter()
.map(|(label, owned_attributes)| {
(
label.to_string(),
EntitySchemaEntry {
type_name: label.to_string(),
is_abstract: false,
parent_type: None,
owned_attributes,
plays_cardinalities: BTreeMap::new(),
doc: None,
meta: Default::default(),
},
)
})
.collect();
SchemaInfo {
entities,
relations: BTreeMap::new(),
attributes,
}
}
fn owned_attribute(label: &str, value_type: ValueType, is_key: bool) -> OwnedAttributeEntry {
OwnedAttributeEntry {
attr_name: label.to_string(),
value_type,
annotations: if is_key {
vec![Annotation::Key]
} else {
Vec::new()
},
is_ordered: false,
doc: None,
meta: Default::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
#[test]
fn descriptor_contains_the_complete_current_state_schema() {
let schema = migration_state_schema();
assert_eq!(
schema
.entities
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>(),
BTreeSet::from([labels::APPLIED_ENTITY, labels::RUN_ENTITY])
);
assert_eq!(
schema
.attributes
.keys()
.map(String::as_str)
.collect::<BTreeSet<_>>(),
BTreeSet::from([
labels::MIGRATION_ID,
labels::APP_LABEL,
labels::NAME,
labels::APPLIED_AT,
labels::CHECKSUM,
labels::RUN_ID,
labels::DIRECTION,
labels::STATUS,
labels::STARTED_AT,
labels::FINISHED_AT,
labels::ERROR,
labels::EXECUTOR_IP,
labels::EXECUTOR_MAC,
])
);
assert!(schema.relations.is_empty());
assert!(
schema
.entities
.contains_key(applied_migration_entity_label())
);
}
#[test]
fn descriptor_preserves_value_types_and_key_ownerships() {
let schema = migration_state_schema();
assert_eq!(
schema.attributes[labels::APPLIED_AT].value_type,
ValueType::DateTime
);
assert_eq!(
schema.attributes[labels::STARTED_AT].value_type,
ValueType::DateTime
);
assert_eq!(
schema.attributes[labels::FINISHED_AT].value_type,
ValueType::DateTime
);
let applied = &schema.entities[labels::APPLIED_ENTITY];
let applied_key = applied
.owned_attributes
.iter()
.find(|attribute| attribute.attr_name == labels::MIGRATION_ID)
.unwrap();
assert_eq!(applied_key.annotations, vec![Annotation::Key]);
let run = &schema.entities[labels::RUN_ENTITY];
let run_key = run
.owned_attributes
.iter()
.find(|attribute| attribute.attr_name == labels::RUN_ID)
.unwrap();
assert_eq!(run_key.annotations, vec![Annotation::Key]);
}
#[test]
fn predicate_is_exact_and_kind_sensitive() {
assert!(is_migration_state_type(
MigrationStateSchemaKind::Entity,
labels::APPLIED_ENTITY
));
assert!(is_migration_state_type(
MigrationStateSchemaKind::Attribute,
labels::CHECKSUM
));
assert!(!is_migration_state_type(
MigrationStateSchemaKind::Attribute,
labels::APPLIED_ENTITY
));
assert!(!is_migration_state_type(
MigrationStateSchemaKind::Entity,
"type_bridge_migration_custom"
));
assert!(!is_migration_state_type(
MigrationStateSchemaKind::Role,
"unqualified-role"
));
}
#[test]
fn canonical_schema_generates_the_existing_typeql_shape() {
let typeql = migration_state_schema().to_typeql().unwrap();
assert!(typeql.contains("attribute migration_applied_at, value datetime;"));
assert!(typeql.contains("attribute migration_started_at, value datetime;"));
assert!(typeql.contains("entity type_bridge_migration,"));
assert!(typeql.contains("owns migration_id @key"));
assert!(typeql.contains("entity type_bridge_migration_run,"));
assert!(typeql.contains("owns migration_run_id @key"));
}
}