Skip to content

type_bridge.migration.diff

diff

Schema comparison DTOs for TypeDB schema management.

Rust/Python shape reconciliation: - Rust schema diffs serialize added/removed type collections as type-name lists; Python public DTOs keep sets of Entity/Relation/Attribute classes. - Rust modified entity/relation maps are keyed by type-name strings; Python DTOs are keyed by the target-side model classes. - Rust entity/relation modified_attributes are triples [name, old_flags, new_flags]; Python uses AttributeFlagChange objects. - Rust root modified_attributes are attribute type definition-change maps; Python uses AttributeTypeChange objects keyed by attribute class where available. - Rust role player/cardinality changes serialize as dicts; Python keeps RolePlayerChange/RoleCardinalityChange DTOs.

AttributeFlagChange dataclass

AttributeFlagChange(name, old_flags, new_flags)

Represents a change in attribute flags (e.g., cardinality change).

AttributeTypeChange dataclass

AttributeTypeChange(name, changes)

Represents a change in a standalone attribute type definition.

RolePlayerChange dataclass

RolePlayerChange(role_name, added_player_types=list(), removed_player_types=list())

Represents a change in role player types.

Tracks when entity types are added to or removed from a role's allowed players.

Example

If a role changes from Role[Person] to Role[Person, Company]: - added_player_types = ["company"] - removed_player_types = []

has_changes

has_changes()

Check if there are any player type changes.

Source code in type_bridge/migration/diff.py
def has_changes(self) -> bool:
    """Check if there are any player type changes."""
    return bool(self.added_player_types or self.removed_player_types)

RoleCardinalityChange dataclass

RoleCardinalityChange(role_name, old_cardinality, new_cardinality)

Represents a change in role cardinality constraints.

Tracks cardinality (min, max) changes on roles. None values indicate unbounded.

RoleAnnotationChange dataclass

RoleAnnotationChange(role_name, doc_changed=None, meta_changed=None)

Represents a change in a role's @doc/@meta annotations (TypeDB 3.12+).

EntityChanges dataclass

EntityChanges(added_attributes=list(), removed_attributes=list(), modified_attributes=list(), doc_changed=None, meta_changed=None)

Represents changes to an entity type.

has_changes

has_changes()

Check if there are any changes.

Source code in type_bridge/migration/diff.py
def has_changes(self) -> bool:
    """Check if there are any changes."""
    return bool(
        self.added_attributes
        or self.removed_attributes
        or self.modified_attributes
        or self.doc_changed
        or self.meta_changed
    )

RelationChanges dataclass

RelationChanges(added_roles=list(), removed_roles=list(), modified_role_players=list(), modified_role_cardinality=list(), modified_role_annotations=list(), added_attributes=list(), removed_attributes=list(), modified_attributes=list(), doc_changed=None, meta_changed=None)

Represents changes to a relation type.

Tracks: - Role additions/removals - Role player type changes (which entities can play each role) - Role cardinality changes - Attribute additions/removals/modifications

has_changes

has_changes()

Check if there are any changes.

Source code in type_bridge/migration/diff.py
def has_changes(self) -> bool:
    """Check if there are any changes."""
    return bool(
        self.added_roles
        or self.removed_roles
        or self.modified_role_players
        or self.modified_role_cardinality
        or self.modified_role_annotations
        or self.added_attributes
        or self.removed_attributes
        or self.modified_attributes
        or self.doc_changed
        or self.meta_changed
    )

SchemaDiff dataclass

SchemaDiff(added_entities=set(), removed_entities=set(), added_relations=set(), removed_relations=set(), added_attributes=set(), removed_attributes=set(), modified_attributes=dict(), modified_entities=dict(), modified_relations=dict(), _rust_diff=None)

Container for schema comparison results.

Represents the differences between two schemas for migration planning.

has_changes

has_changes()

Check if there are any schema differences.

Returns:

Type Description
bool

True if any changes exist, False otherwise

Source code in type_bridge/migration/diff.py
def has_changes(self) -> bool:
    """Check if there are any schema differences.

    Returns:
        True if any changes exist, False otherwise
    """
    return bool(
        self.added_entities
        or self.removed_entities
        or self.added_relations
        or self.removed_relations
        or self.added_attributes
        or self.removed_attributes
        or self.modified_attributes
        or self.modified_entities
        or self.modified_relations
    )

summary

summary()

Generate a human-readable summary of changes.

Returns:

Type Description
str

Formatted summary string

Source code in type_bridge/migration/diff.py
def summary(self) -> str:
    """Generate a human-readable summary of changes.

    Returns:
        Formatted summary string
    """
    lines = []
    lines.append("Schema Comparison Summary")
    lines.append("=" * 50)

    if not self.has_changes():
        lines.append("No schema changes detected.")
        return "\n".join(lines)

    if self.added_entities:
        lines.append(f"\nAdded Entities ({len(self.added_entities)}):")
        for entity in sorted(self.added_entities, key=lambda e: e.__name__):
            lines.append(f"  + {entity.__name__}")

    if self.removed_entities:
        lines.append(f"\nRemoved Entities ({len(self.removed_entities)}):")
        for entity in sorted(self.removed_entities, key=lambda e: e.__name__):
            lines.append(f"  - {entity.__name__}")

    if self.added_relations:
        lines.append(f"\nAdded Relations ({len(self.added_relations)}):")
        for relation in sorted(self.added_relations, key=lambda r: r.__name__):
            lines.append(f"  + {relation.__name__}")

    if self.removed_relations:
        lines.append(f"\nRemoved Relations ({len(self.removed_relations)}):")
        for relation in sorted(self.removed_relations, key=lambda r: r.__name__):
            lines.append(f"  - {relation.__name__}")

    if self.added_attributes:
        lines.append(f"\nAdded Attributes ({len(self.added_attributes)}):")
        for attr in sorted(self.added_attributes, key=lambda a: a.get_attribute_name()):
            lines.append(f"  + {attr.get_attribute_name()}")

    if self.removed_attributes:
        lines.append(f"\nRemoved Attributes ({len(self.removed_attributes)}):")
        for attr in sorted(self.removed_attributes, key=lambda a: a.get_attribute_name()):
            lines.append(f"  - {attr.get_attribute_name()}")

    if self.modified_attributes:
        lines.append(f"\nModified Attributes ({len(self.modified_attributes)}):")
        for attr, changes in sorted(
            self.modified_attributes.items(), key=lambda item: item[0].get_attribute_name()
        ):
            fields = ", ".join(_attribute_type_changed_fields(changes.changes))
            lines.append(f"  ~ {attr.get_attribute_name()}: {fields}")

    if self.modified_entities:
        lines.append(f"\nModified Entities ({len(self.modified_entities)}):")
        for entity, changes in self.modified_entities.items():
            lines.append(f"  ~ {entity.__name__}")
            if changes.added_attributes:
                lines.append(f"    added_attributes: {changes.added_attributes}")
            if changes.removed_attributes:
                lines.append(f"    removed_attributes: {changes.removed_attributes}")
            if changes.modified_attributes:
                lines.append("    modified_attributes:")
                for attr_change in changes.modified_attributes:
                    lines.append(f"      - {attr_change.name}:")
                    lines.append(f"          old: {attr_change.old_flags}")
                    lines.append(f"          new: {attr_change.new_flags}")

    if self.modified_relations:
        lines.append(f"\nModified Relations ({len(self.modified_relations)}):")
        for relation, rel_changes in self.modified_relations.items():
            relation_changes: RelationChanges = rel_changes
            lines.append(f"  ~ {relation.__name__}")
            if relation_changes.added_roles:
                lines.append(f"    added_roles: {relation_changes.added_roles}")
            if relation_changes.removed_roles:
                lines.append(f"    removed_roles: {relation_changes.removed_roles}")
            if relation_changes.modified_role_players:
                lines.append("    modified_role_players:")
                for rpc in relation_changes.modified_role_players:
                    lines.append(f"      - {rpc.role_name}:")
                    if rpc.added_player_types:
                        lines.append(f"          added: {rpc.added_player_types}")
                    if rpc.removed_player_types:
                        lines.append(f"          removed: {rpc.removed_player_types}")
            if relation_changes.modified_role_cardinality:
                lines.append("    modified_role_cardinality:")
                for rcc in relation_changes.modified_role_cardinality:
                    lines.append(f"      - {rcc.role_name}:")
                    lines.append(f"          old: {rcc.old_cardinality}")
                    lines.append(f"          new: {rcc.new_cardinality}")
            if relation_changes.added_attributes:
                lines.append(f"    added_attributes: {relation_changes.added_attributes}")
            if relation_changes.removed_attributes:
                lines.append(f"    removed_attributes: {relation_changes.removed_attributes}")
            if relation_changes.modified_attributes:
                lines.append("    modified_attributes:")
                for attr_change in relation_changes.modified_attributes:
                    lines.append(f"      - {attr_change.name}:")
                    lines.append(f"          old: {attr_change.old_flags}")
                    lines.append(f"          new: {attr_change.new_flags}")

    return "\n".join(lines)

to_rust_dict

to_rust_dict()

Serialize this public DTO to the Rust SchemaDiff dict shape.

Converts Python class-keyed sets/dicts into the string-keyed lists and nested dicts the PyO3 layer expects, reconciling field shapes as described in the module docstring.

Source code in type_bridge/migration/diff.py
def to_rust_dict(self) -> dict[str, Any]:
    """Serialize this public DTO to the Rust ``SchemaDiff`` dict shape.

    Converts Python class-keyed sets/dicts into the string-keyed lists and
    nested dicts the PyO3 layer expects, reconciling field shapes as described
    in the module docstring.
    """
    return {
        "added_entities": sorted(_model_type_name(model) for model in self.added_entities),
        "removed_entities": sorted(_model_type_name(model) for model in self.removed_entities),
        "modified_entities": {
            _model_type_name(model): _entity_changes_to_rust(changes)
            for model, changes in self.modified_entities.items()
        },
        "added_relations": sorted(_model_type_name(model) for model in self.added_relations),
        "removed_relations": sorted(
            _model_type_name(model) for model in self.removed_relations
        ),
        "modified_relations": {
            _model_type_name(model): _relation_changes_to_rust(changes)
            for model, changes in self.modified_relations.items()
        },
        "added_attributes": sorted(attr.get_attribute_name() for attr in self.added_attributes),
        "removed_attributes": sorted(
            attr.get_attribute_name() for attr in self.removed_attributes
        ),
        "modified_attributes": {
            attr.get_attribute_name(): change.changes
            for attr, change in self.modified_attributes.items()
        },
    }

from_rust_schema_diff

from_rust_schema_diff(rust_diff, *, current_schema=None, target_schema=None)

Map the serialized Rust diff into the frozen Python DTO shape.

Source code in type_bridge/migration/diff.py
def from_rust_schema_diff(
    rust_diff: dict[str, Any],
    *,
    current_schema: Any | None = None,
    target_schema: Any | None = None,
) -> SchemaDiff:
    """Map the serialized Rust diff into the frozen Python DTO shape."""
    current_entities = _entities_by_name(current_schema)
    target_entities = _entities_by_name(target_schema)
    current_relations = _relations_by_name(current_schema)
    target_relations = _relations_by_name(target_schema)
    current_attributes = _attributes_by_name(current_schema)
    target_attributes = _attributes_by_name(target_schema)

    diff = SchemaDiff(
        added_entities={
            target_entities[name]
            for name in rust_diff.get("added_entities", [])
            if name in target_entities
        },
        removed_entities={
            current_entities[name]
            for name in rust_diff.get("removed_entities", [])
            if name in current_entities
        },
        added_relations={
            target_relations[name]
            for name in rust_diff.get("added_relations", [])
            if name in target_relations
        },
        removed_relations={
            current_relations[name]
            for name in rust_diff.get("removed_relations", [])
            if name in current_relations
        },
        added_attributes={
            target_attributes[name]
            for name in rust_diff.get("added_attributes", [])
            if name in target_attributes
        },
        removed_attributes={
            current_attributes[name]
            for name in rust_diff.get("removed_attributes", [])
            if name in current_attributes
        },
        _rust_diff=rust_diff,
    )

    for attr_name, changes in rust_diff.get("modified_attributes", {}).items():
        attr = target_attributes.get(attr_name) or current_attributes.get(attr_name)
        if attr is not None:
            diff.modified_attributes[attr] = AttributeTypeChange(
                name=attr_name,
                changes=dict(changes),
            )

    for type_name, changes in rust_diff.get("modified_entities", {}).items():
        entity = target_entities.get(type_name) or current_entities.get(type_name)
        if entity is not None:
            diff.modified_entities[entity] = _entity_changes_from_rust(changes)

    for type_name, changes in rust_diff.get("modified_relations", {}).items():
        relation = target_relations.get(type_name) or current_relations.get(type_name)
        if relation is not None:
            diff.modified_relations[relation] = _relation_changes_from_rust(changes)

    return diff