Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update diff logic to handle schema kind migration #5634

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/infrahub/core/diff/model/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,10 @@ def root_id(self) -> str:
def node_id(self) -> str:
return str(self.node_node.get("uuid"))

@property
def node_db_id(self) -> str:
return self.node_node.element_id

@property
def node_kind(self) -> str:
return str(self.node_node.get("kind"))
Expand Down
19 changes: 19 additions & 0 deletions backend/infrahub/core/diff/query_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
force_action: DiffAction | None
uuid: str
kind: str
db_id: str
from_time: Timestamp
status: RelationshipStatus
attributes_by_name: dict[str, DiffAttributeIntermediate] = field(default_factory=dict)
relationships_by_name: dict[str, DiffRelationshipIntermediate] = field(default_factory=dict)

Expand Down Expand Up @@ -567,11 +570,27 @@ def _get_diff_node(self, database_path: DatabasePath, diff_root: DiffRootInterme
diff_root.nodes_by_id[node_id] = DiffNodeIntermediate(
uuid=node_id,
kind=database_path.node_kind,
db_id=database_path.node_db_id,
from_time=database_path.node_changed_at,
status=database_path.node_status,
force_action=DiffAction.UPDATED
if database_path.node_branch_support is BranchSupportType.AGNOSTIC
else None,
)
diff_node = diff_root.nodes_by_id[node_id]
# special handling for nodes that have their kind updated, which results in 2 nodes with the same uuid
if diff_node.db_id != database_path.node_db_id and (
database_path.node_changed_at > diff_node.from_time
or (
database_path.node_changed_at >= diff_node.from_time
and (diff_node.status, database_path.node_status)
== (RelationshipStatus.DELETED, RelationshipStatus.ACTIVE)
)
):
diff_node.kind = database_path.node_kind
diff_node.db_id = database_path.node_db_id
diff_node.from_time = database_path.node_changed_at
diff_node.status = database_path.node_status
diff_node.track_database_path(database_path=database_path)
return diff_node

Expand Down
41 changes: 39 additions & 2 deletions backend/infrahub/core/query/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ def __init__(
r in relationships(latest_base_path)
WHERE r.from < $branch_from_time
)
// ------------------------
// special handling for nodes that had their kind updated,
// the migration leaves two nodes with the same UUID linked to the same Relationship
// ------------------------
AND (
n.uuid IS NULL OR base_prop.uuid IS NULL OR n.uuid <> base_prop.uuid
OR type(base_r_node) <> "IS_RELATED" OR type(base_r_prop) <> "IS_RELATED"
)
WITH latest_base_path, base_r_root, base_r_node, base_r_prop
ORDER BY base_r_prop.from DESC, base_r_node.from DESC, base_r_root.from DESC
LIMIT 1
Expand Down Expand Up @@ -175,8 +183,13 @@ def __init__(
OR peer_r_node.to IS NULL
OR peer_r_node.to >= r_peer.from
)
// ------------------------
// special handling for nodes that had their kind updated,
// the migration leaves two nodes with the same UUID linked to the same Relationship
// ------------------------
AND (n.uuid IS NULL OR peer.uuid IS NULL OR n.uuid <> peer.uuid)
WITH peer_path, r_peer, r_prop
ORDER BY r_peer.branch = r_prop.branch DESC, r_peer.from DESC
ORDER BY r_peer.branch = r_prop.branch DESC, r_peer.from DESC, r_peer.status ASC
LIMIT 1
RETURN peer_path
}
Expand Down Expand Up @@ -309,6 +322,14 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
AND [%(id_func)s(p), type(r_node)] <> [%(id_func)s(prop), type(r_prop)]
AND top_diff_rel.status = r_node.status
AND top_diff_rel.status = r_prop.status
// ------------------------
// special handling for nodes that had their kind updated,
// the migration leaves two nodes with the same UUID linked to the same Relationship
// ------------------------
AND (
p.uuid IS NULL OR prop.uuid IS NULL OR p.uuid <> prop.uuid
OR type(r_node) <> "IS_RELATED" OR type(r_prop) <> "IS_RELATED"
)
WITH path, p, node, prop, r_prop, r_node, type(r_node) AS rel_type, row_from_time
// -------------------------------------
// Exclude attributes/relationships added then removed on branch within timeframe
Expand Down Expand Up @@ -483,6 +504,14 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
AND [%(id_func)s(p), type(mid_diff_rel)] <> [%(id_func)s(prop), type(r_prop)]
// exclude paths where an active edge is below a deleted edge
AND (mid_diff_rel.status = "active" OR r_prop.status = "deleted")
// ------------------------
// special handling for nodes that had their kind updated,
// the migration leaves two nodes with the same UUID linked to the same Relationship
// ------------------------
AND (
p.uuid IS NULL OR prop.uuid IS NULL OR p.uuid <> prop.uuid
OR type(mid_diff_rel) <> "IS_RELATED" OR type(r_prop) <> "IS_RELATED"
)
WITH path, prop, r_prop, mid_r_root
ORDER BY
type(r_prop),
Expand All @@ -493,7 +522,7 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
RETURN latest_prop_path
}
// -------------------------------------
// Exclude properties within the timeframe
// Exclude properties added and deleted within the timeframe
// -------------------------------------
WITH q, nodes(latest_prop_path)[3] AS prop, type(relationships(latest_prop_path)[2]) AS rel_type, latest_prop_path, has_more_data, row_from_time
CALL {
Expand Down Expand Up @@ -594,6 +623,14 @@ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
)
)
)
// ------------------------
// special handling for nodes that had their kind updated,
// the migration leaves two nodes with the same UUID linked to the same Relationship
// ------------------------
AND (
n.uuid IS NULL OR q.uuid IS NULL OR n.uuid <> q.uuid
OR type(r_node) <> "IS_RELATED" OR type(diff_rel) <> "IS_RELATED"
)
AND ALL(
r_pair IN [[r_root, r_node], [r_node, diff_rel]]
// filter out paths where a base branch edge follows a branch edge
Expand Down
74 changes: 72 additions & 2 deletions backend/tests/unit/core/diff/test_diff_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from infrahub import config
from infrahub.core import registry
from infrahub.core.branch import Branch
from infrahub.core.constants import DiffAction, InfrahubKind, RelationshipCardinality
from infrahub.core.constants import DiffAction, InfrahubKind, RelationshipCardinality, SchemaPathType
from infrahub.core.constants.database import DatabaseEdgeType
from infrahub.core.diff.calculator import DiffCalculator
from infrahub.core.initialization import create_branch
from infrahub.core.manager import NodeManager
from infrahub.core.migrations.schema.node_kind_update import NodeKindUpdateMigration
from infrahub.core.node import Node
from infrahub.core.path import SchemaPath
from infrahub.core.schema.schema_branch import SchemaBranch
from infrahub.core.timestamp import Timestamp
from infrahub.database import InfrahubDatabase
Expand Down Expand Up @@ -1308,7 +1310,6 @@ async def test_relationship_property_owner_conflicting_updates(
# check branch
branch_root_path = calculated_diffs.diff_branch_diff
assert branch_root_path.branch == branch.name
assert len(branch_root_path.nodes) == 2
nodes_by_id = {n.uuid: n for n in branch_root_path.nodes}
assert set(nodes_by_id.keys()) == {person_john_main.get_id(), car_accord_main.get_id()}
# john node on branch
Expand Down Expand Up @@ -3077,3 +3078,72 @@ async def test_diff_relationship_property_update_on_main(
assert diff_rel.name == "owner"
assert diff_rel.action is DiffAction.UPDATED
assert {elem.action for elem in diff_rel.relationships} == {DiffAction.ADDED, DiffAction.REMOVED}


async def test_calculate_with_migrated_kind_node(
db: InfrahubDatabase, default_branch: Branch, car_accord_main, car_camry_main, person_john_main, person_jane_main
):
"""Test that the diff can correctly handle a schema kind migration, which results in 2 nodes with the same UUID"""
branch = await create_branch(db=db, branch_name="branch")
branch_car = await Node.init(db=db, schema="TestCar", branch=branch)
await branch_car.new(db=db, name="nova", nbr_seats=2, is_electric=False, owner=person_jane_main.id)
await branch_car.save(db=db)
schema = registry.schema.get_schema_branch(name=default_branch.name)
car_schema = schema.get(name="TestCar")
car_schema.name = "NewCar"
car_schema.namespace = "Test2"
assert car_schema.kind == "Test2NewCar"
owner_rel_schema = car_schema.get_relationship(name="owner")
registry.schema.set(name="Test2NewCar", schema=car_schema, branch=branch.name)

migration = NodeKindUpdateMigration(
previous_node_schema=schema.get(name="TestCar"),
new_node_schema=car_schema,
schema_path=SchemaPath(path_type=SchemaPathType.ATTRIBUTE, schema_kind="Test2NewCar", field_name="namespace"),
)
execution_result = await migration.execute(db=db, branch=branch)
assert not execution_result.errors

diff_calculator = DiffCalculator(db=db)

calculated_diffs = await diff_calculator.calculate_diff(
base_branch=default_branch,
diff_branch=branch,
from_time=Timestamp(branch.get_branched_from()),
to_time=Timestamp(),
previous_node_specifiers={
car_accord_main.id: {owner_rel_schema.get_identifier()},
car_camry_main.id: {owner_rel_schema.get_identifier()},
},
include_unchanged=True,
)

branch_diff = calculated_diffs.diff_branch_diff
assert len(branch_diff.nodes) == 2
nodes_by_id = {n.uuid: n for n in branch_diff.nodes}
assert set(nodes_by_id.keys()) == {branch_car.id, person_jane_main.id}
# validate relationship on person is correct
jane_diff = nodes_by_id[person_jane_main.id]
assert jane_diff.action is DiffAction.UPDATED
assert len(jane_diff.attributes) == 0
rels_by_name = {r.name: r for r in jane_diff.relationships}
assert set(rels_by_name.keys()) == {"cars"}
cars_rel_diff = rels_by_name["cars"]
assert cars_rel_diff.action is DiffAction.UPDATED
elements_by_peer_id = {e.peer_id: e for e in cars_rel_diff.relationships}
assert set(elements_by_peer_id) == {branch_car.id}
element_diff = elements_by_peer_id[branch_car.id]
assert element_diff.action is DiffAction.ADDED
props_by_type = {p.property_type: p for p in element_diff.properties}
for property_type, value in (
(DatabaseEdgeType.IS_RELATED, branch_car.id),
(DatabaseEdgeType.IS_VISIBLE, True),
(DatabaseEdgeType.IS_PROTECTED, False),
):
property_diff = props_by_type[property_type]
assert property_diff.action is DiffAction.ADDED
assert property_diff.new_value == value
assert property_diff.previous_value is None

base_diff = calculated_diffs.base_branch_diff
assert len(base_diff.nodes) == 0
Loading