infrahub-server 1.1.6__py3-none-any.whl → 1.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. infrahub/core/attribute.py +4 -1
  2. infrahub/core/branch/tasks.py +7 -4
  3. infrahub/core/diff/combiner.py +11 -7
  4. infrahub/core/diff/coordinator.py +49 -70
  5. infrahub/core/diff/data_check_synchronizer.py +86 -7
  6. infrahub/core/diff/enricher/aggregated.py +3 -3
  7. infrahub/core/diff/enricher/cardinality_one.py +1 -6
  8. infrahub/core/diff/enricher/labels.py +13 -3
  9. infrahub/core/diff/enricher/path_identifier.py +2 -8
  10. infrahub/core/diff/merger/merger.py +5 -3
  11. infrahub/core/diff/model/path.py +42 -24
  12. infrahub/core/diff/query/all_conflicts.py +5 -2
  13. infrahub/core/diff/query/diff_get.py +2 -1
  14. infrahub/core/diff/query/field_specifiers.py +2 -0
  15. infrahub/core/diff/query/field_summary.py +2 -1
  16. infrahub/core/diff/query/filters.py +12 -1
  17. infrahub/core/diff/query/has_conflicts_query.py +5 -2
  18. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +3 -3
  19. infrahub/core/diff/query/roots_metadata.py +8 -1
  20. infrahub/core/diff/query/save.py +148 -63
  21. infrahub/core/diff/query/summary_counts_enricher.py +220 -0
  22. infrahub/core/diff/query/time_range_query.py +2 -1
  23. infrahub/core/diff/query_parser.py +49 -24
  24. infrahub/core/diff/repository/deserializer.py +23 -24
  25. infrahub/core/diff/repository/repository.py +76 -20
  26. infrahub/core/node/__init__.py +6 -1
  27. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  28. infrahub/core/node/ipam.py +6 -1
  29. infrahub/core/node/permissions.py +4 -0
  30. infrahub/core/query/diff.py +41 -3
  31. infrahub/core/query/node.py +8 -2
  32. infrahub/core/query/relationship.py +2 -1
  33. infrahub/core/query/resource_manager.py +3 -1
  34. infrahub/core/utils.py +1 -0
  35. infrahub/core/validators/uniqueness/query.py +20 -17
  36. infrahub/database/__init__.py +13 -0
  37. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  38. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  39. infrahub/graphql/mutations/computed_attribute.py +3 -1
  40. infrahub/graphql/mutations/diff.py +28 -4
  41. infrahub/graphql/mutations/main.py +11 -6
  42. infrahub/graphql/mutations/relationship.py +29 -1
  43. infrahub/graphql/mutations/tasks.py +6 -3
  44. infrahub/graphql/queries/resource_manager.py +7 -3
  45. infrahub/permissions/__init__.py +2 -1
  46. infrahub/permissions/types.py +26 -0
  47. infrahub_sdk/batch.py +2 -2
  48. infrahub_sdk/config.py +1 -1
  49. infrahub_sdk/ctl/check.py +1 -1
  50. infrahub_sdk/ctl/utils.py +2 -2
  51. infrahub_sdk/data.py +1 -1
  52. infrahub_sdk/node.py +4 -1
  53. infrahub_sdk/protocols.py +1 -0
  54. infrahub_sdk/schema/__init__.py +3 -0
  55. infrahub_sdk/testing/docker.py +0 -30
  56. infrahub_sdk/transfer/exporter/json.py +1 -1
  57. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.7.dist-info}/METADATA +41 -7
  58. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.7.dist-info}/RECORD +65 -65
  59. infrahub_testcontainers/container.py +12 -3
  60. infrahub_testcontainers/docker-compose.test.yml +22 -3
  61. infrahub_testcontainers/haproxy.cfg +43 -0
  62. infrahub_testcontainers/helpers.py +85 -1
  63. infrahub/core/diff/enricher/summary_counts.py +0 -105
  64. infrahub/dependencies/builder/diff/enricher/summary_counts.py +0 -8
  65. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.7.dist-info}/LICENSE.txt +0 -0
  66. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.7.dist-info}/WHEEL +0 -0
  67. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.7.dist-info}/entry_points.txt +0 -0
@@ -470,6 +470,7 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
470
470
  related_node_ids: Optional[set] = None,
471
471
  filter_sensitive: bool = False,
472
472
  permissions: Optional[dict] = None,
473
+ include_properties: bool = True,
473
474
  ) -> dict:
474
475
  """Generate GraphQL Payload for this attribute."""
475
476
  # pylint: disable=too-many-branches
@@ -480,7 +481,9 @@ class BaseAttribute(FlagPropertyMixin, NodePropertyMixin):
480
481
  field_names = fields.keys()
481
482
  else:
482
483
  # REMOVED updated_at for now, need to investigate further how it's being used today
483
- field_names = ["__typename", "value"] + self._node_properties + self._flag_properties
484
+ field_names = ["__typename", "value"]
485
+ if include_properties:
486
+ field_names += self._node_properties + self._flag_properties
484
487
 
485
488
  for field_name in field_names:
486
489
  if field_name == "updated_at":
@@ -70,14 +70,17 @@ async def rebase_branch(branch: str) -> None:
70
70
  service=service,
71
71
  )
72
72
  diff_repository = await component_registry.get_component(DiffRepository, db=db, branch=obj)
73
- enriched_diff = await diff_coordinator.update_branch_diff_and_return(base_branch=base_branch, diff_branch=obj)
74
- if enriched_diff.get_all_conflicts():
73
+ enriched_diff_metadata = await diff_coordinator.update_branch_diff(base_branch=base_branch, diff_branch=obj)
74
+ async for _ in diff_repository.get_all_conflicts_for_diff(
75
+ diff_branch_name=enriched_diff_metadata.diff_branch_name, diff_id=enriched_diff_metadata.uuid
76
+ ):
77
+ # if there are any conflicts, raise the error
75
78
  raise ValidationError(
76
79
  f"Branch {obj.name} contains conflicts with the default branch that must be addressed."
77
80
  " Please review the diff for details and manually update the conflicts before rebasing."
78
81
  )
79
82
  node_diff_field_summaries = await diff_repository.get_node_field_summaries(
80
- diff_branch_name=enriched_diff.diff_branch_name, diff_id=enriched_diff.uuid
83
+ diff_branch_name=enriched_diff_metadata.diff_branch_name, diff_id=enriched_diff_metadata.uuid
81
84
  )
82
85
 
83
86
  candidate_schema = merger.get_candidate_schema()
@@ -219,7 +222,7 @@ async def merge_branch(branch: str) -> None:
219
222
  # remove tracking ID from the diff because there is no diff after the merge
220
223
  # -------------------------------------------------------------
221
224
  diff_repository = await component_registry.get_component(DiffRepository, db=db, branch=obj)
222
- await diff_repository.drop_tracking_ids(tracking_ids=[BranchTrackingId(name=obj.name)])
225
+ await diff_repository.mark_tracking_ids_merged(tracking_ids=[BranchTrackingId(name=obj.name)])
223
226
 
224
227
  # -------------------------------------------------------------
225
228
  # Generate an event to indicate that a branch has been merged
@@ -1,7 +1,6 @@
1
1
  from copy import deepcopy
2
2
  from dataclasses import dataclass, field, replace
3
3
  from typing import Iterable
4
- from uuid import uuid4
5
4
 
6
5
  from infrahub.core.constants import NULL_VALUE, DiffAction, RelationshipCardinality
7
6
  from infrahub.core.constants.database import DatabaseEdgeType
@@ -342,6 +341,8 @@ class DiffCombiner:
342
341
 
343
342
  def _copy_node_without_parents(self, node: EnrichedDiffNode) -> EnrichedDiffNode:
344
343
  rels_without_parents = {replace(r, nodes=set()) for r in node.relationships}
344
+ for rel in rels_without_parents:
345
+ rel.reset_summaries()
345
346
  node_without_parents = replace(node, relationships=rels_without_parents)
346
347
  return deepcopy(node_without_parents)
347
348
 
@@ -351,15 +352,11 @@ class DiffCombiner:
351
352
  if node_pair.earlier is None:
352
353
  if node_pair.later is not None:
353
354
  copied = self._copy_node_without_parents(node_pair.later)
354
- for rel in copied.relationships:
355
- rel.reset_summaries()
356
355
  combined_nodes.add(copied)
357
356
  continue
358
357
  if node_pair.later is None:
359
358
  if node_pair.earlier is not None:
360
359
  copied = self._copy_node_without_parents(node_pair.earlier)
361
- for rel in copied.relationships:
362
- rel.reset_summaries()
363
360
  combined_nodes.add(copied)
364
361
  continue
365
362
  combined_attributes = self._combine_attributes(
@@ -420,14 +417,21 @@ class DiffCombiner:
420
417
  filtered_node_pairs = self._filter_nodes_to_keep(earlier_diff=earlier, later_diff=later)
421
418
  combined_nodes = self._combine_nodes(node_pairs=filtered_node_pairs)
422
419
  self._link_child_nodes(nodes=combined_nodes)
420
+ if earlier.exists_on_database:
421
+ diff_uuid = earlier.uuid
422
+ partner_uuid = earlier.partner_uuid
423
+ else:
424
+ diff_uuid = later.uuid
425
+ partner_uuid = later.partner_uuid
423
426
  combined_diffs.append(
424
427
  EnrichedDiffRoot(
425
- uuid=str(uuid4()),
426
- partner_uuid=later.partner_uuid,
428
+ uuid=diff_uuid,
429
+ partner_uuid=partner_uuid,
427
430
  base_branch_name=later.base_branch_name,
428
431
  diff_branch_name=later.diff_branch_name,
429
432
  from_time=earlier.from_time,
430
433
  to_time=later.to_time,
434
+ tracking_id=later.tracking_id,
431
435
  nodes=combined_nodes,
432
436
  )
433
437
  )
@@ -6,6 +6,7 @@ from uuid import uuid4
6
6
 
7
7
  from infrahub import lock
8
8
  from infrahub.core.timestamp import Timestamp
9
+ from infrahub.exceptions import ValidationError
9
10
  from infrahub.log import get_logger
10
11
 
11
12
  from .model.path import (
@@ -29,7 +30,6 @@ if TYPE_CHECKING:
29
30
  from .data_check_synchronizer import DiffDataCheckSynchronizer
30
31
  from .enricher.aggregated import AggregatedDiffEnricher
31
32
  from .enricher.labels import DiffLabelsEnricher
32
- from .enricher.summary_counts import DiffSummaryCountsEnricher
33
33
  from .repository.repository import DiffRepository
34
34
 
35
35
 
@@ -42,7 +42,7 @@ class EnrichedDiffRequest:
42
42
  diff_branch: Branch
43
43
  from_time: Timestamp
44
44
  to_time: Timestamp
45
- tracking_id: TrackingId | None = field(default=None)
45
+ tracking_id: TrackingId
46
46
  node_field_specifiers: dict[str, set[str]] = field(default_factory=dict)
47
47
 
48
48
  def __repr__(self) -> str:
@@ -65,7 +65,6 @@ class DiffCoordinator:
65
65
  diff_combiner: DiffCombiner,
66
66
  conflicts_enricher: ConflictsEnricher,
67
67
  labels_enricher: DiffLabelsEnricher,
68
- summary_counts_enricher: DiffSummaryCountsEnricher,
69
68
  data_check_synchronizer: DiffDataCheckSynchronizer,
70
69
  conflict_transferer: DiffConflictTransferer,
71
70
  ) -> None:
@@ -75,7 +74,6 @@ class DiffCoordinator:
75
74
  self.diff_combiner = diff_combiner
76
75
  self.conflicts_enricher = conflicts_enricher
77
76
  self.labels_enricher = labels_enricher
78
- self.summary_counts_enricher = summary_counts_enricher
79
77
  self.data_check_synchronizer = data_check_synchronizer
80
78
  self.conflict_transferer = conflict_transferer
81
79
  self.lock_registry = lock.registry
@@ -101,6 +99,8 @@ class DiffCoordinator:
101
99
  to_timestamp = Timestamp(to_time)
102
100
  else:
103
101
  to_timestamp = Timestamp()
102
+ if not name:
103
+ raise ValidationError("diff with specified time range requires a name")
104
104
  await self.create_or_update_arbitrary_timeframe_diff(
105
105
  base_branch=base_branch,
106
106
  diff_branch=diff_branch,
@@ -115,27 +115,7 @@ class DiffCoordinator:
115
115
  lock_name += "__incremental"
116
116
  return lock_name
117
117
 
118
- async def update_branch_diff_and_return(self, base_branch: Branch, diff_branch: Branch) -> EnrichedDiffRoot:
119
- enriched_diff = await self.update_branch_diff(base_branch=base_branch, diff_branch=diff_branch)
120
- if isinstance(enriched_diff, EnrichedDiffRoot):
121
- return enriched_diff
122
- return await self._finalize_diff_root_metadata(diff_root_metadata=enriched_diff)
123
-
124
- async def _finalize_diff_root_metadata(self, diff_root_metadata: EnrichedDiffRootMetadata) -> EnrichedDiffRoot:
125
- # if this is EnrichedDiffMetadata, we need to retrieve the full diff and set its metadata to match
126
- full_enriched_diff = await self.diff_repo.get_one(
127
- diff_branch_name=diff_root_metadata.diff_branch_name, diff_id=diff_root_metadata.uuid
128
- )
129
- full_enriched_diff.update_metadata(
130
- from_time=diff_root_metadata.from_time,
131
- to_time=diff_root_metadata.to_time,
132
- tracking_id=diff_root_metadata.tracking_id,
133
- )
134
- return full_enriched_diff
135
-
136
- async def update_branch_diff(
137
- self, base_branch: Branch, diff_branch: Branch
138
- ) -> EnrichedDiffRoot | EnrichedDiffRootMetadata:
118
+ async def update_branch_diff(self, base_branch: Branch, diff_branch: Branch) -> EnrichedDiffRootMetadata:
139
119
  log.info(f"Received request to update branch diff for {base_branch.name} - {diff_branch.name}")
140
120
  incremental_lock_name = self._get_lock_name(
141
121
  base_branch_name=base_branch.name, diff_branch_name=diff_branch.name, is_incremental=True
@@ -169,12 +149,6 @@ class DiffCoordinator:
169
149
  tracking_id=tracking_id,
170
150
  force_branch_refresh=False,
171
151
  )
172
- if not isinstance(enriched_diffs, EnrichedDiffs):
173
- await self._update_core_data_checks(enriched_diff=enriched_diffs.diff_branch_diff)
174
- return enriched_diffs.diff_branch_diff
175
-
176
- await self.summary_counts_enricher.enrich(enriched_diff_root=enriched_diffs.base_branch_diff)
177
- await self.summary_counts_enricher.enrich(enriched_diff_root=enriched_diffs.diff_branch_diff)
178
152
  await self.diff_repo.save(enriched_diffs=enriched_diffs)
179
153
  await self._update_core_data_checks(enriched_diff=enriched_diffs.diff_branch_diff)
180
154
  log.info(f"Branch diff update complete for {base_branch.name} - {diff_branch.name}")
@@ -186,11 +160,9 @@ class DiffCoordinator:
186
160
  diff_branch: Branch,
187
161
  from_time: Timestamp,
188
162
  to_time: Timestamp,
189
- name: str | None = None,
190
- ) -> EnrichedDiffRoot:
191
- tracking_id = None
192
- if name:
193
- tracking_id = NameTrackingId(name=name)
163
+ name: str,
164
+ ) -> EnrichedDiffRootMetadata:
165
+ tracking_id = NameTrackingId(name=name)
194
166
  general_lock_name = self._get_lock_name(
195
167
  base_branch_name=base_branch.name, diff_branch_name=diff_branch.name, is_incremental=False
196
168
  )
@@ -204,13 +176,7 @@ class DiffCoordinator:
204
176
  tracking_id=tracking_id,
205
177
  force_branch_refresh=False,
206
178
  )
207
- # metadata-only diff, so no nodes to enrich
208
- if not isinstance(enriched_diffs, EnrichedDiffs):
209
- await self._update_core_data_checks(enriched_diff=enriched_diffs.diff_branch_diff)
210
- return await self._finalize_diff_root_metadata(diff_root_metadata=enriched_diffs.diff_branch_diff)
211
179
 
212
- await self.summary_counts_enricher.enrich(enriched_diff_root=enriched_diffs.base_branch_diff)
213
- await self.summary_counts_enricher.enrich(enriched_diff_root=enriched_diffs.diff_branch_diff)
214
180
  await self.diff_repo.save(enriched_diffs=enriched_diffs)
215
181
  await self._update_core_data_checks(enriched_diff=enriched_diffs.diff_branch_diff)
216
182
  log.info(f"Arbitrary diff update complete for {base_branch.name} - {diff_branch.name}")
@@ -252,8 +218,6 @@ class DiffCoordinator:
252
218
  earlier=current_branch_diff, later=enriched_diffs.diff_branch_diff
253
219
  )
254
220
 
255
- await self.summary_counts_enricher.enrich(enriched_diff_root=enriched_diffs.base_branch_diff)
256
- await self.summary_counts_enricher.enrich(enriched_diff_root=enriched_diffs.diff_branch_diff)
257
221
  await self.diff_repo.save(enriched_diffs=enriched_diffs)
258
222
  await self._update_core_data_checks(enriched_diff=enriched_diffs.diff_branch_diff)
259
223
  log.info(f"Diff recalculation complete for {base_branch.name} - {diff_branch.name}")
@@ -316,7 +280,7 @@ class DiffCoordinator:
316
280
  diff_branch: Branch,
317
281
  from_time: Timestamp,
318
282
  to_time: Timestamp,
319
- tracking_id: TrackingId | None = None,
283
+ tracking_id: TrackingId,
320
284
  force_branch_refresh: Literal[True] = ...,
321
285
  ) -> EnrichedDiffs: ...
322
286
 
@@ -327,7 +291,7 @@ class DiffCoordinator:
327
291
  diff_branch: Branch,
328
292
  from_time: Timestamp,
329
293
  to_time: Timestamp,
330
- tracking_id: TrackingId | None = None,
294
+ tracking_id: TrackingId,
331
295
  force_branch_refresh: Literal[False] = ...,
332
296
  ) -> EnrichedDiffs | EnrichedDiffsMetadata: ...
333
297
 
@@ -337,7 +301,7 @@ class DiffCoordinator:
337
301
  diff_branch: Branch,
338
302
  from_time: Timestamp,
339
303
  to_time: Timestamp,
340
- tracking_id: TrackingId | None = None,
304
+ tracking_id: TrackingId,
341
305
  force_branch_refresh: bool = False,
342
306
  ) -> EnrichedDiffs | EnrichedDiffsMetadata:
343
307
  # start with empty diffs b/c we only care about their metadata for now, hydrate them with data as needed
@@ -346,6 +310,7 @@ class DiffCoordinator:
346
310
  diff_branch_names=[diff_branch.name],
347
311
  from_time=from_time,
348
312
  to_time=to_time,
313
+ tracking_id=tracking_id,
349
314
  )
350
315
  aggregated_enriched_diffs = await self._aggregate_enriched_diffs(
351
316
  diff_request=EnrichedDiffRequest(
@@ -357,22 +322,23 @@ class DiffCoordinator:
357
322
  ),
358
323
  partial_enriched_diffs=diff_pairs_metadata if not force_branch_refresh else None,
359
324
  )
360
- if tracking_id:
361
- diff_uuids_to_delete: list[str] = []
362
- for diff_pair in diff_pairs_metadata:
363
- if (
364
- diff_pair.base_branch_diff.tracking_id == tracking_id
365
- and diff_pair.base_branch_diff.uuid != aggregated_enriched_diffs.base_branch_diff.uuid
366
- ):
367
- diff_uuids_to_delete.append(diff_pair.base_branch_diff.uuid)
368
- if (
369
- diff_pair.diff_branch_diff.tracking_id == tracking_id
370
- and diff_pair.diff_branch_diff.uuid != aggregated_enriched_diffs.diff_branch_diff.uuid
371
- ):
372
- diff_uuids_to_delete.append(diff_pair.diff_branch_diff.uuid)
373
-
374
- if diff_uuids_to_delete:
375
- await self.diff_repo.delete_diff_roots(diff_root_uuids=diff_uuids_to_delete)
325
+ diff_uuids_to_delete: list[str] = []
326
+ for diff_pair in diff_pairs_metadata:
327
+ if (
328
+ diff_pair.base_branch_diff.tracking_id == tracking_id
329
+ and diff_pair.base_branch_diff.uuid != aggregated_enriched_diffs.base_branch_diff.uuid
330
+ and diff_pair.base_branch_diff.exists_on_database
331
+ ):
332
+ diff_uuids_to_delete.append(diff_pair.base_branch_diff.uuid)
333
+ if (
334
+ diff_pair.diff_branch_diff.tracking_id == tracking_id
335
+ and diff_pair.diff_branch_diff.uuid != aggregated_enriched_diffs.diff_branch_diff.uuid
336
+ and diff_pair.diff_branch_diff.exists_on_database
337
+ ):
338
+ diff_uuids_to_delete.append(diff_pair.diff_branch_diff.uuid)
339
+
340
+ if diff_uuids_to_delete:
341
+ await self.diff_repo.delete_diff_roots(diff_root_uuids=diff_uuids_to_delete)
376
342
 
377
343
  # this is an EnrichedDiffsMetadata, so there are no nodes to enrich
378
344
  if not isinstance(aggregated_enriched_diffs, EnrichedDiffs):
@@ -459,6 +425,7 @@ class DiffCoordinator:
459
425
  diff_branch=diff_request.diff_branch,
460
426
  from_time=current_time,
461
427
  to_time=end_time,
428
+ tracking_id=diff_request.tracking_id,
462
429
  )
463
430
  )
464
431
  current_time = end_time
@@ -481,7 +448,6 @@ class DiffCoordinator:
481
448
  aggregated_enriched_diffs.update_metadata(
482
449
  from_time=diff_request.from_time, to_time=diff_request.to_time, tracking_id=diff_request.tracking_id
483
450
  )
484
- aggregated_enriched_diffs.set_fresh_uuids()
485
451
  return aggregated_enriched_diffs
486
452
 
487
453
  async def _concatenate_diffs_and_requests(
@@ -498,6 +464,7 @@ class DiffCoordinator:
498
464
  meaning multiple diffs (some that may have been freshly calculated) were combined
499
465
  """
500
466
  previous_diff_pair: EnrichedDiffs | EnrichedDiffsMetadata | None = None
467
+ updated_node_uuids: set[str] = set()
501
468
  for diff_or_request in diff_or_request_list:
502
469
  if isinstance(diff_or_request, EnrichedDiffRequest):
503
470
  if previous_diff_pair:
@@ -508,9 +475,12 @@ class DiffCoordinator:
508
475
  log.info(f"Number node field specifiers: {len(node_field_specifiers)}")
509
476
  diff_or_request.node_field_specifiers = node_field_specifiers
510
477
  is_incremental_diff = diff_or_request.from_time != full_diff_request.from_time
511
- single_enriched_diffs: EnrichedDiffs | EnrichedDiffsMetadata = await self._calculate_enriched_diff(
478
+ calculated_diff = await self._calculate_enriched_diff(
512
479
  diff_request=diff_or_request, is_incremental_diff=is_incremental_diff
513
480
  )
481
+ updated_node_uuids |= calculated_diff.base_node_uuids
482
+ updated_node_uuids |= calculated_diff.branch_node_uuids
483
+ single_enriched_diffs: EnrichedDiffs | EnrichedDiffsMetadata = calculated_diff
514
484
 
515
485
  elif isinstance(diff_or_request, EnrichedDiffsMetadata):
516
486
  single_enriched_diffs = diff_or_request
@@ -522,13 +492,20 @@ class DiffCoordinator:
522
492
  continue
523
493
 
524
494
  log.info("Combining diffs...")
525
- previous_diff_pair = await self._combine_diffs(earlier=previous_diff_pair, later=single_enriched_diffs)
495
+ previous_diff_pair = await self._combine_diffs(
496
+ earlier=previous_diff_pair,
497
+ later=single_enriched_diffs,
498
+ node_uuids=updated_node_uuids,
499
+ )
526
500
  log.info("Diffs combined.")
527
501
 
528
502
  return previous_diff_pair
529
503
 
530
504
  async def _combine_diffs(
531
- self, earlier: EnrichedDiffs | EnrichedDiffsMetadata, later: EnrichedDiffs | EnrichedDiffsMetadata
505
+ self,
506
+ earlier: EnrichedDiffs | EnrichedDiffsMetadata,
507
+ later: EnrichedDiffs | EnrichedDiffsMetadata,
508
+ node_uuids: set[str],
532
509
  ) -> EnrichedDiffs | EnrichedDiffsMetadata:
533
510
  log.info(f"Earlier diff to combine: {earlier!r}")
534
511
  log.info(f"Later diff to combine: {later!r}")
@@ -545,11 +522,11 @@ class DiffCoordinator:
545
522
  # hydrate the diffs to combine, if necessary
546
523
  if not isinstance(earlier, EnrichedDiffs):
547
524
  log.info("Hydrating earlier diff...")
548
- earlier = await self.diff_repo.hydrate_diff_pair(enriched_diffs_metadata=earlier)
525
+ earlier = await self.diff_repo.hydrate_diff_pair(enriched_diffs_metadata=earlier, node_uuids=node_uuids)
549
526
  log.info("Earlier diff hydrated.")
550
527
  if not isinstance(later, EnrichedDiffs):
551
528
  log.info("Hydrating later diff...")
552
- later = await self.diff_repo.hydrate_diff_pair(enriched_diffs_metadata=later)
529
+ later = await self.diff_repo.hydrate_diff_pair(enriched_diffs_metadata=later, node_uuids=node_uuids)
553
530
  log.info("Later diff hydrated.")
554
531
 
555
532
  return await self.diff_combiner.combine(earlier_diffs=earlier, later_diffs=later)
@@ -570,6 +547,8 @@ class DiffCoordinator:
570
547
  previous_node_specifiers=diff_request.node_field_specifiers,
571
548
  )
572
549
  log.info("Calculation complete. Enriching diff...")
573
- enriched_diff_pair = await self.diff_enricher.enrich(calculated_diffs=calculated_diff_pair)
550
+ enriched_diff_pair = await self.diff_enricher.enrich(
551
+ calculated_diffs=calculated_diff_pair, tracking_id=diff_request.tracking_id
552
+ )
574
553
  log.info("Enrichment complete")
575
554
  return enriched_diff_pair
@@ -10,7 +10,13 @@ from infrahub.proposed_change.constants import ProposedChangeState
10
10
 
11
11
  from .conflicts_extractor import DiffConflictsExtractor
12
12
  from .model.diff import DataConflict
13
- from .model.path import ConflictSelection, EnrichedDiffConflict, EnrichedDiffRoot, EnrichedDiffRootMetadata
13
+ from .model.path import (
14
+ ConflictSelection,
15
+ EnrichedDiffConflict,
16
+ EnrichedDiffNode,
17
+ EnrichedDiffRoot,
18
+ EnrichedDiffRootMetadata,
19
+ )
14
20
  from .repository.repository import DiffRepository
15
21
 
16
22
 
@@ -54,20 +60,31 @@ class DiffDataCheckSynchronizer:
54
60
  if not proposed_changes:
55
61
  return []
56
62
  all_data_checks = []
63
+ enriched_diff_all_conflicts: EnrichedDiffRoot | None = None
57
64
  for pc in proposed_changes:
58
65
  # if the enriched_diff is EnrichedDiffRootMetadata, then it has no new data in it
59
66
  if not isinstance(enriched_diff, EnrichedDiffRoot):
60
67
  has_validator = bool(await self.conflict_recorder.get_validator(proposed_change=pc))
61
- # if this pc does not have a validator, then it is a new ProposedChange
68
+ # if this pc has a validator, then the conflicts for this diff have already been synchronized and we can be done
62
69
  if has_validator:
63
70
  continue
64
- # if this is a new ProposedChange, we need to hydrate then EnrichedDiffRoot so that we can get the conflicts from it
65
- enriched_diff = await self.diff_repository.get_one(
66
- diff_branch_name=enriched_diff.diff_branch_name, diff_id=enriched_diff.uuid
71
+
72
+ # we need to get the conflicts for this diff from the database b/c `enriched_diff` might not include all nodes
73
+ if not enriched_diff_all_conflicts:
74
+ retrieved_diff_conflicts_only = await self.diff_repository.get_one(
75
+ diff_branch_name=enriched_diff.diff_branch_name,
76
+ diff_id=enriched_diff.uuid,
77
+ filters={"only_conflicted": True},
67
78
  )
79
+ enriched_diff_all_conflicts = retrieved_diff_conflicts_only
80
+ # if `enriched_diff` is an EnrichedDiffRootsMetadata, then there have been no changes to the diff and
81
+ # we can use `retrieved_diff_conflicts_only`
82
+ # otherwise, we need to combine the changes to `enriched_diff` with the conflicts from the database
83
+ if isinstance(enriched_diff, EnrichedDiffRoot):
84
+ self._update_diff_conflicts(updated_diff=enriched_diff, retrieved_diff=enriched_diff_all_conflicts)
68
85
 
69
- data_conflicts = await self._get_data_conflicts(enriched_diff=enriched_diff)
70
- enriched_conflicts_map = self._get_enriched_conflicts_map(enriched_diff=enriched_diff)
86
+ data_conflicts = await self._get_data_conflicts(enriched_diff=enriched_diff_all_conflicts)
87
+ enriched_conflicts_map = self._get_enriched_conflicts_map(enriched_diff=enriched_diff_all_conflicts)
71
88
  core_data_checks = await self.conflict_recorder.record_conflicts(
72
89
  proposed_change_id=pc.get_id(), conflicts=data_conflicts
73
90
  )
@@ -95,3 +112,65 @@ class DiffDataCheckSynchronizer:
95
112
  if enriched_conflict.selected_branch is ConflictSelection.DIFF_BRANCH:
96
113
  return BranchConflictKeep.SOURCE
97
114
  return None
115
+
116
+ def _update_diff_conflicts(self, updated_diff: EnrichedDiffRoot, retrieved_diff: EnrichedDiffRoot) -> None:
117
+ for updated_node in updated_diff.nodes:
118
+ try:
119
+ retrieved_node = retrieved_diff.get_node(node_uuid=updated_node.uuid)
120
+ except ValueError:
121
+ retrieved_node = None
122
+ if not retrieved_node:
123
+ retrieved_diff.nodes.add(updated_node)
124
+ continue
125
+ retrieved_node.conflict = updated_node.conflict
126
+ self._update_diff_attr_conflicts(updated_node=updated_node, retrieved_node=retrieved_node)
127
+ self._update_diff_relationship_conflicts(updated_node=updated_node, retrieved_node=retrieved_node)
128
+
129
+ def _update_diff_attr_conflicts(self, updated_node: EnrichedDiffNode, retrieved_node: EnrichedDiffNode) -> None:
130
+ for updated_attr in updated_node.attributes:
131
+ try:
132
+ retrieved_attr = retrieved_node.get_attribute(name=updated_attr.name)
133
+ except ValueError:
134
+ retrieved_attr = None
135
+ if not retrieved_attr:
136
+ retrieved_node.attributes.add(updated_attr)
137
+ continue
138
+ for updated_prop in updated_attr.properties:
139
+ try:
140
+ retrieved_prop = retrieved_attr.get_property(updated_prop.property_type)
141
+ except ValueError:
142
+ retrieved_prop = None
143
+ if not retrieved_prop:
144
+ retrieved_attr.properties.add(updated_prop)
145
+ continue
146
+ retrieved_prop.conflict = updated_prop.conflict
147
+
148
+ def _update_diff_relationship_conflicts(
149
+ self, updated_node: EnrichedDiffNode, retrieved_node: EnrichedDiffNode
150
+ ) -> None:
151
+ for updated_rel in updated_node.relationships:
152
+ try:
153
+ retrieved_rel = retrieved_node.get_relationship(name=updated_rel.name)
154
+ except ValueError:
155
+ retrieved_rel = None
156
+ if not retrieved_rel:
157
+ retrieved_node.relationships.add(updated_rel)
158
+ continue
159
+ for updated_element in updated_rel.relationships:
160
+ try:
161
+ retrieved_element = retrieved_rel.get_element(updated_element.peer_id)
162
+ except ValueError:
163
+ retrieved_element = None
164
+ if not retrieved_element:
165
+ retrieved_rel.relationships.add(updated_element)
166
+ continue
167
+ retrieved_element.conflict = updated_element.conflict
168
+ for updated_prop in updated_element.properties:
169
+ try:
170
+ retrieved_prop = retrieved_element.get_property(updated_prop.property_type)
171
+ except ValueError:
172
+ retrieved_prop = None
173
+ if not retrieved_prop:
174
+ retrieved_element.properties.add(updated_prop)
175
+ continue
176
+ retrieved_prop.conflict = updated_prop.conflict
@@ -1,4 +1,4 @@
1
- from ..model.path import CalculatedDiffs, EnrichedDiffs
1
+ from ..model.path import CalculatedDiffs, EnrichedDiffs, TrackingId
2
2
  from .interface import DiffEnricherInterface
3
3
 
4
4
 
@@ -6,8 +6,8 @@ class AggregatedDiffEnricher:
6
6
  def __init__(self, enrichers: list[DiffEnricherInterface]) -> None:
7
7
  self.enrichers = enrichers
8
8
 
9
- async def enrich(self, calculated_diffs: CalculatedDiffs) -> EnrichedDiffs:
10
- enriched_diffs = EnrichedDiffs.from_calculated_diffs(calculated_diffs=calculated_diffs)
9
+ async def enrich(self, calculated_diffs: CalculatedDiffs, tracking_id: TrackingId) -> EnrichedDiffs:
10
+ enriched_diffs = EnrichedDiffs.from_calculated_diffs(calculated_diffs=calculated_diffs, tracking_id=tracking_id)
11
11
 
12
12
  for enricher in self.enrichers:
13
13
  await enricher.enrich(enriched_diff_root=enriched_diffs.base_branch_diff, calculated_diffs=calculated_diffs)
@@ -103,14 +103,9 @@ class DiffCardinalityOneEnricher(DiffEnricherInterface):
103
103
  )
104
104
  )
105
105
  if consolidated_properties:
106
- element_timestamps = {element.changed_at for element in diff_relationship.relationships}
107
106
  element_actions = {element.action for element in diff_relationship.relationships}
108
107
  # check if this is a simultaneous update
109
- if (
110
- len(diff_relationship.relationships) > 1
111
- and len(element_timestamps) == 1
112
- and {DiffAction.REMOVED, DiffAction.ADDED} <= element_actions
113
- ):
108
+ if len(diff_relationship.relationships) > 1 and {DiffAction.REMOVED, DiffAction.ADDED} <= element_actions:
114
109
  latest_element = [
115
110
  element for element in diff_relationship.relationships if element.action is DiffAction.ADDED
116
111
  ][0]
@@ -154,10 +154,20 @@ class DiffLabelsEnricher(DiffEnricherInterface):
154
154
  for node in enriched_diff.nodes:
155
155
  if not node.relationships:
156
156
  continue
157
- node_schema = self.db.schema.get(name=node.kind, branch=self.diff_branch_name, duplicate=False)
157
+
158
+ node_schema = self.db.schema.get(name=node.kind, branch=enriched_diff.diff_branch_name, duplicate=False)
159
+ alternate_node_schema = None
160
+ if enriched_diff.diff_branch_name != enriched_diff.base_branch_name and self.db.schema.has(
161
+ name=node.kind, branch=enriched_diff.base_branch_name
162
+ ):
163
+ alternate_node_schema = self.db.schema.get(
164
+ name=node.kind, branch=enriched_diff.base_branch_name, duplicate=False
165
+ )
158
166
  for relationship_diff in node.relationships:
159
- relationship_schema = node_schema.get_relationship(name=relationship_diff.name)
160
- relationship_diff.label = relationship_schema.label or ""
167
+ relationship_schema = node_schema.get_relationship_or_none(name=relationship_diff.name)
168
+ if not relationship_schema and alternate_node_schema:
169
+ relationship_schema = alternate_node_schema.get_relationship_or_none(name=relationship_diff.name)
170
+ relationship_diff.label = relationship_schema.label or "" if relationship_schema else ""
161
171
 
162
172
  async def _get_display_label_map(
163
173
  self, display_label_requests: set[DisplayLabelRequest]
@@ -1,4 +1,4 @@
1
- from infrahub.core.constants import PathType, RelationshipCardinality
1
+ from infrahub.core.constants import PathType
2
2
  from infrahub.core.path import DataPath
3
3
  from infrahub.database import InfrahubDatabase
4
4
 
@@ -44,14 +44,8 @@ class DiffPathIdentifierEnricher(DiffEnricherInterface):
44
44
  attribute_property.path_identifier = property_path.get_path()
45
45
  if not node.relationships:
46
46
  continue
47
- node_schema = self.db.schema.get(name=node.kind, branch=self.diff_branch_name, duplicate=False)
48
47
  for relationship in node.relationships:
49
- relationship_schema = node_schema.get_relationship(name=relationship.name)
50
- path_type = (
51
- PathType.RELATIONSHIP_ONE
52
- if relationship_schema.cardinality is RelationshipCardinality.ONE
53
- else PathType.RELATIONSHIP_MANY
54
- )
48
+ path_type = PathType.from_relationship(relationship.cardinality)
55
49
  relationship_path = DataPath(
56
50
  branch=enriched_diff_root.diff_branch_name,
57
51
  path_type=path_type,
@@ -34,13 +34,15 @@ class DiffMerger:
34
34
  self.serializer = serializer
35
35
 
36
36
  async def merge_graph(self, at: Timestamp) -> None:
37
+ tracking_id = BranchTrackingId(name=self.source_branch.name)
37
38
  enriched_diffs = await self.diff_repository.get_roots_metadata(
38
- diff_branch_names=[self.source_branch.name], base_branch_names=[self.destination_branch.name]
39
+ diff_branch_names=[self.source_branch.name],
40
+ base_branch_names=[self.destination_branch.name],
41
+ tracking_id=tracking_id,
39
42
  )
40
43
  latest_diff = None
41
- tracking_id = BranchTrackingId(name=self.source_branch.name)
42
44
  for diff in enriched_diffs:
43
- if latest_diff is None or (diff.tracking_id == tracking_id and diff.to_time > latest_diff.to_time):
45
+ if latest_diff is None or (diff.to_time > latest_diff.to_time):
44
46
  latest_diff = diff
45
47
  if latest_diff is None:
46
48
  raise RuntimeError(f"Missing diff for branch {self.source_branch.name}")