Skip to content

Commit 01e37a6

Browse files
authored
fix diff calculation bug around deleted edges (#4532)
* use branched_from instead of created_at for branch diff * fix bug in delete relationship query * remove UNCHANGED properties, attrs, relationships from calculated diffs * consolidate same_value checking logic in ConflictsEnricher * refactor diff query to exclude intra-branch updates, add previous_values flag * fixes for diff recalculation * use branched_from instead of created_at for diff calculation * more fixes and tests for diff calculation * normalize time comparisons
1 parent 980c601 commit 01e37a6

File tree

10 files changed

+649
-131
lines changed

10 files changed

+649
-131
lines changed

backend/infrahub/core/branch.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ def validate_branch_name(cls, value: str) -> str:
8383
def set_branched_from(cls, value: str) -> str:
8484
return Timestamp(value).to_string()
8585

86+
def get_branched_from(self) -> str:
87+
if not self.branched_from:
88+
raise RuntimeError(f"branched_from not set for branch {self.name}")
89+
return self.branched_from
90+
8691
@field_validator("created_at", mode="before")
8792
@classmethod
8893
def set_created_at(cls, value: str) -> str:

backend/infrahub/core/diff/calculator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ async def calculate_diff(
2121
previous_node_specifiers: set[NodeFieldSpecifier] | None = None,
2222
) -> CalculatedDiffs:
2323
if diff_branch.name == registry.default_branch:
24-
diff_branch_create_time = from_time
24+
diff_branch_from_time = from_time
2525
else:
26-
diff_branch_create_time = Timestamp(diff_branch.get_created_at())
26+
diff_branch_from_time = Timestamp(diff_branch.get_branched_from())
2727
diff_parser = DiffQueryParser(
2828
base_branch=base_branch,
2929
diff_branch=diff_branch,
@@ -35,7 +35,7 @@ async def calculate_diff(
3535
db=self.db,
3636
branch=diff_branch,
3737
base_branch=base_branch,
38-
diff_branch_create_time=diff_branch_create_time,
38+
diff_branch_from_time=diff_branch_from_time,
3939
diff_from=from_time,
4040
diff_to=to_time,
4141
)
@@ -51,7 +51,7 @@ async def calculate_diff(
5151
db=self.db,
5252
branch=base_branch,
5353
base_branch=base_branch,
54-
diff_branch_create_time=diff_branch_create_time,
54+
diff_branch_from_time=diff_branch_from_time,
5555
diff_from=from_time,
5656
diff_to=to_time,
5757
current_node_field_specifiers=[

backend/infrahub/core/diff/conflicts_enricher.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,7 @@ def _add_attribute_conflicts(
9797
for property_type in common_property_types:
9898
base_property = base_property_map[property_type]
9999
branch_property = branch_property_map[property_type]
100-
same_value = base_property.new_value == branch_property.new_value or (
101-
base_property.action is DiffAction.UNCHANGED
102-
and base_property.previous_value == branch_property.previous_value
103-
)
100+
same_value = self._have_same_value(base_property=base_property, branch_property=branch_property)
104101
if not same_value:
105102
self._add_property_conflict(
106103
base_property=base_property,
@@ -147,10 +144,7 @@ def _add_relationship_conflicts_for_one_peer(
147144
for property_type in common_property_types:
148145
base_property = base_properties_by_type[property_type]
149146
branch_property = branch_properties_by_type[property_type]
150-
same_value = base_property.new_value == branch_property.new_value or (
151-
base_property.action is DiffAction.UNCHANGED
152-
and base_property.previous_value == branch_property.previous_value
153-
)
147+
same_value = self._have_same_value(base_property=base_property, branch_property=branch_property)
154148
# special handling for cardinality-one peer ID conflict
155149
if branch_property.property_type is DatabaseEdgeType.IS_RELATED and is_cardinality_one:
156150
if same_value:
@@ -215,3 +209,15 @@ def _add_property_conflict(
215209
diff_branch_changed_at=branch_property.changed_at,
216210
selected_branch=selected_branch,
217211
)
212+
213+
def _have_same_value(self, base_property: EnrichedDiffProperty, branch_property: EnrichedDiffProperty) -> bool:
214+
if base_property.new_value == branch_property.new_value:
215+
return True
216+
if {base_property.new_value, branch_property.new_value} <= {"NULL", None}:
217+
return True
218+
if (
219+
base_property.action is DiffAction.UNCHANGED
220+
and base_property.previous_value == branch_property.previous_value
221+
):
222+
return True
223+
return False

backend/infrahub/core/diff/coordinator.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,21 @@ async def recalculate(
187187
async with self.lock_registry.get(name=general_lock_name, namespace=self.lock_namespace):
188188
log.debug(f"Acquired lock to recalculate diff for {base_branch.name} - {diff_branch.name}")
189189
current_branch_diff = await self.diff_repo.get_one(diff_branch_name=diff_branch.name, diff_id=diff_id)
190+
current_base_diff = await self.diff_repo.get_one(
191+
diff_branch_name=base_branch.name, diff_id=current_branch_diff.partner_uuid
192+
)
190193
if current_branch_diff.tracking_id and isinstance(current_branch_diff.tracking_id, BranchTrackingId):
191194
to_time = Timestamp()
192195
else:
193196
to_time = current_branch_diff.to_time
194-
await self.diff_repo.delete_diff_roots(diff_root_uuids=[current_branch_diff.uuid])
197+
await self.diff_repo.delete_diff_roots(diff_root_uuids=[current_branch_diff.uuid, current_base_diff.uuid])
198+
from_time = current_branch_diff.from_time
199+
branched_from_time = Timestamp(diff_branch.get_branched_from())
200+
from_time = max(from_time, branched_from_time)
195201
enriched_diffs = await self._update_diffs(
196202
base_branch=base_branch,
197203
diff_branch=diff_branch,
198-
from_time=current_branch_diff.from_time,
204+
from_time=branched_from_time,
199205
to_time=to_time,
200206
tracking_id=current_branch_diff.tracking_id,
201207
force_branch_refresh=True,
@@ -283,14 +289,14 @@ async def _get_aggregated_enriched_diffs(
283289
node_field_specifiers = self._get_node_field_specifiers(
284290
enriched_diff=previous_diffs.diff_branch_diff
285291
)
286-
diff_request = EnrichedDiffRequest(
292+
inner_diff_request = EnrichedDiffRequest(
287293
base_branch=diff_request.base_branch,
288294
diff_branch=diff_request.diff_branch,
289295
from_time=current_time,
290296
to_time=end_time,
291297
node_field_specifiers=node_field_specifiers,
292298
)
293-
current_diffs = await self._get_enriched_diff(diff_request=diff_request)
299+
current_diffs = await self._get_enriched_diff(diff_request=inner_diff_request)
294300

295301
if previous_diffs:
296302
current_diffs = await self.diff_combiner.combine(

backend/infrahub/core/diff/query_parser.py

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,14 @@ def track_database_path(self, database_path: DatabasePath) -> None:
160160
self.timestamp_status_map[database_path.attribute_changed_at] = database_path.attribute_status
161161

162162
def to_diff_attribute(self, from_time: Timestamp) -> DiffAttribute:
163-
properties = [prop.to_diff_property(from_time=from_time) for prop in self.properties_by_type.values()]
163+
properties = []
164+
for prop in self.properties_by_type.values():
165+
diff_prop = prop.to_diff_property(from_time=from_time)
166+
if diff_prop.action is not DiffAction.UNCHANGED:
167+
properties.append(diff_prop)
164168
action, changed_at = self.get_action_and_timestamp(from_time=from_time)
169+
if not properties:
170+
action = DiffAction.UNCHANGED
165171
return DiffAttribute(
166172
uuid=self.uuid, name=self.name, changed_at=changed_at, action=action, properties=properties
167173
)
@@ -262,11 +268,15 @@ def get_final_single_relationship(self, from_time: Timestamp) -> DiffSingleRelat
262268
chronological_properties=peer_id_properties, from_time=from_time
263269
)
264270
peer_id = peer_final_property.new_value or peer_final_property.previous_value
265-
other_final_properties = [
266-
self._get_single_relationship_final_property(chronological_properties=property_list, from_time=from_time)
267-
for property_type, property_list in self.ordered_properties_by_type.items()
268-
if property_type != DatabaseEdgeType.IS_RELATED
269-
]
271+
other_final_properties = []
272+
for property_type, property_list in self.ordered_properties_by_type.items():
273+
if property_type is DatabaseEdgeType.IS_RELATED:
274+
continue
275+
final_prop = self._get_single_relationship_final_property(
276+
chronological_properties=property_list, from_time=from_time
277+
)
278+
if final_prop.action is not DiffAction.UNCHANGED:
279+
other_final_properties.append(final_prop)
270280
final_properties = [peer_final_property] + other_final_properties
271281
last_changed_property = max(final_properties, key=lambda fp: fp.changed_at)
272282
last_changed_at = last_changed_property.changed_at
@@ -330,8 +340,8 @@ def to_diff_relationship(self, from_time: Timestamp) -> DiffRelationship:
330340
single_relationships = [
331341
sr.get_final_single_relationship(from_time=from_time) for sr in self._single_relationship_list
332342
]
333-
last_changed_relatonship = max(single_relationships, key=lambda r: r.changed_at)
334-
last_changed_at = last_changed_relatonship.changed_at
343+
last_changed_relationship = max(single_relationships, key=lambda r: r.changed_at)
344+
last_changed_at = last_changed_relationship.changed_at
335345
action = DiffAction.UPDATED
336346
if last_changed_at < from_time:
337347
action = DiffAction.UNCHANGED
@@ -358,9 +368,19 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
358368
relationships_by_name: dict[str, DiffRelationshipIntermediate] = field(default_factory=dict)
359369

360370
def to_diff_node(self, from_time: Timestamp) -> DiffNode:
361-
attributes = [attr.to_diff_attribute(from_time=from_time) for attr in self.attributes_by_name.values()]
362-
relationships = [rel.to_diff_relationship(from_time=from_time) for rel in self.relationships_by_name.values()]
371+
attributes = []
372+
for attr in self.attributes_by_name.values():
373+
diff_attr = attr.to_diff_attribute(from_time=from_time)
374+
if diff_attr.action is not DiffAction.UNCHANGED:
375+
attributes.append(diff_attr)
376+
relationships = []
377+
for rel in self.relationships_by_name.values():
378+
diff_rel = rel.to_diff_relationship(from_time=from_time)
379+
if diff_rel.action is not DiffAction.UNCHANGED:
380+
relationships.append(diff_rel)
363381
action, changed_at = self.get_action_and_timestamp(from_time=from_time)
382+
if not attributes and not relationships:
383+
action = DiffAction.UNCHANGED
364384
return DiffNode(
365385
uuid=self.uuid,
366386
kind=self.kind,
@@ -384,8 +404,11 @@ class DiffRootIntermediate:
384404
def to_diff_root(self, from_time: Timestamp, to_time: Timestamp) -> DiffRoot:
385405
nodes = []
386406
for node in self.nodes_by_id.values():
387-
if not node.is_empty:
388-
nodes.append(node.to_diff_node(from_time=from_time))
407+
if node.is_empty:
408+
continue
409+
diff_node = node.to_diff_node(from_time=from_time)
410+
if diff_node.action is not DiffAction.UNCHANGED:
411+
nodes.append(diff_node)
389412
return DiffRoot(uuid=self.uuid, branch=self.branch, nodes=nodes, from_time=from_time, to_time=to_time)
390413

391414

@@ -403,10 +426,11 @@ def __init__(
403426
self.schema_manager = schema_manager
404427
self.from_time = from_time
405428
self.to_time = to_time or Timestamp()
429+
# if this diff is for the base branch, use from_time b/c create_time would be too much
406430
if diff_branch.name == base_branch.name:
407-
self.diff_branch_create_time = from_time
431+
self.diff_branched_from_time = from_time
408432
else:
409-
self.diff_branch_create_time = Timestamp(diff_branch.get_created_at())
433+
self.diff_branched_from_time = Timestamp(diff_branch.get_branched_from())
410434
self._diff_root_by_branch: dict[str, DiffRootIntermediate] = {}
411435
self._final_diff_root_by_branch: dict[str, DiffRoot] = {}
412436

@@ -610,19 +634,19 @@ def _remove_empty_base_diff_root(self) -> None:
610634
ordered_diff_values = property_diff.get_ordered_values_asc()
611635
if not ordered_diff_values:
612636
continue
613-
if ordered_diff_values[-1].changed_at >= self.diff_branch_create_time:
637+
if ordered_diff_values[-1].changed_at >= self.diff_branched_from_time:
614638
return
615639
for relationship_diff in node_diff.relationships_by_name.values():
616640
for diff_relationship_property_list in relationship_diff.properties_by_db_id.values():
617641
for diff_relationship_property in diff_relationship_property_list:
618-
if diff_relationship_property.changed_at >= self.diff_branch_create_time:
642+
if diff_relationship_property.changed_at >= self.diff_branched_from_time:
619643
return
620644
del self._diff_root_by_branch[self.base_branch_name]
621645

622646
def _finalize(self) -> None:
623647
for branch, diff_root_intermediate in self._diff_root_by_branch.items():
624648
if branch == self.base_branch_name:
625-
from_time = self.diff_branch_create_time
649+
from_time = self.diff_branched_from_time
626650
else:
627651
from_time = self.from_time
628652
self._final_diff_root_by_branch[branch] = diff_root_intermediate.to_diff_root(

0 commit comments

Comments
 (0)