infrahub-server 1.1.6__py3-none-any.whl → 1.1.8__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 (97) 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 +6 -6
  8. infrahub/core/diff/enricher/hierarchy.py +17 -4
  9. infrahub/core/diff/enricher/labels.py +18 -3
  10. infrahub/core/diff/enricher/path_identifier.py +7 -8
  11. infrahub/core/diff/merger/merger.py +5 -3
  12. infrahub/core/diff/model/path.py +66 -25
  13. infrahub/core/diff/parent_node_adder.py +78 -0
  14. infrahub/core/diff/payload_builder.py +13 -2
  15. infrahub/core/diff/query/all_conflicts.py +5 -2
  16. infrahub/core/diff/query/diff_get.py +2 -1
  17. infrahub/core/diff/query/field_specifiers.py +2 -0
  18. infrahub/core/diff/query/field_summary.py +2 -1
  19. infrahub/core/diff/query/filters.py +12 -1
  20. infrahub/core/diff/query/has_conflicts_query.py +5 -2
  21. infrahub/core/diff/query/{drop_tracking_id.py → merge_tracking_id.py} +3 -3
  22. infrahub/core/diff/query/roots_metadata.py +8 -1
  23. infrahub/core/diff/query/save.py +230 -139
  24. infrahub/core/diff/query/summary_counts_enricher.py +267 -0
  25. infrahub/core/diff/query/time_range_query.py +2 -1
  26. infrahub/core/diff/query_parser.py +49 -24
  27. infrahub/core/diff/repository/deserializer.py +31 -27
  28. infrahub/core/diff/repository/repository.py +215 -41
  29. infrahub/core/diff/tasks.py +4 -4
  30. infrahub/core/graph/__init__.py +1 -1
  31. infrahub/core/graph/index.py +3 -0
  32. infrahub/core/migrations/graph/__init__.py +4 -0
  33. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +256 -0
  34. infrahub/core/migrations/graph/m020_duplicate_edges.py +160 -0
  35. infrahub/core/migrations/query/node_duplicate.py +38 -18
  36. infrahub/core/migrations/schema/node_remove.py +26 -12
  37. infrahub/core/migrations/shared.py +10 -8
  38. infrahub/core/node/__init__.py +19 -9
  39. infrahub/core/node/constraints/grouped_uniqueness.py +25 -5
  40. infrahub/core/node/ipam.py +6 -1
  41. infrahub/core/node/permissions.py +4 -0
  42. infrahub/core/query/attribute.py +2 -0
  43. infrahub/core/query/diff.py +41 -3
  44. infrahub/core/query/node.py +74 -21
  45. infrahub/core/query/relationship.py +107 -17
  46. infrahub/core/query/resource_manager.py +5 -1
  47. infrahub/core/relationship/model.py +8 -12
  48. infrahub/core/schema/definitions/core.py +1 -0
  49. infrahub/core/utils.py +1 -0
  50. infrahub/core/validators/uniqueness/query.py +20 -17
  51. infrahub/database/__init__.py +14 -0
  52. infrahub/dependencies/builder/constraint/grouped/node_runner.py +0 -2
  53. infrahub/dependencies/builder/diff/coordinator.py +0 -2
  54. infrahub/dependencies/builder/diff/deserializer.py +3 -1
  55. infrahub/dependencies/builder/diff/enricher/hierarchy.py +3 -1
  56. infrahub/dependencies/builder/diff/parent_node_adder.py +8 -0
  57. infrahub/graphql/mutations/computed_attribute.py +3 -1
  58. infrahub/graphql/mutations/diff.py +41 -10
  59. infrahub/graphql/mutations/main.py +11 -6
  60. infrahub/graphql/mutations/relationship.py +29 -1
  61. infrahub/graphql/mutations/resource_manager.py +3 -3
  62. infrahub/graphql/mutations/tasks.py +6 -3
  63. infrahub/graphql/queries/resource_manager.py +7 -3
  64. infrahub/permissions/__init__.py +2 -1
  65. infrahub/permissions/types.py +26 -0
  66. infrahub_sdk/client.py +10 -2
  67. infrahub_sdk/config.py +3 -0
  68. infrahub_sdk/ctl/check.py +3 -3
  69. infrahub_sdk/ctl/cli_commands.py +16 -11
  70. infrahub_sdk/ctl/exceptions.py +0 -6
  71. infrahub_sdk/ctl/exporter.py +1 -1
  72. infrahub_sdk/ctl/generator.py +5 -5
  73. infrahub_sdk/ctl/importer.py +3 -2
  74. infrahub_sdk/ctl/menu.py +1 -1
  75. infrahub_sdk/ctl/object.py +1 -1
  76. infrahub_sdk/ctl/repository.py +23 -15
  77. infrahub_sdk/ctl/schema.py +2 -2
  78. infrahub_sdk/ctl/utils.py +4 -3
  79. infrahub_sdk/ctl/validate.py +2 -1
  80. infrahub_sdk/exceptions.py +12 -0
  81. infrahub_sdk/generator.py +3 -0
  82. infrahub_sdk/node.py +7 -4
  83. infrahub_sdk/testing/schemas/animal.py +9 -0
  84. infrahub_sdk/utils.py +11 -1
  85. infrahub_sdk/yaml.py +2 -3
  86. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/METADATA +41 -7
  87. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/RECORD +94 -91
  88. infrahub_testcontainers/container.py +12 -3
  89. infrahub_testcontainers/docker-compose.test.yml +22 -3
  90. infrahub_testcontainers/haproxy.cfg +43 -0
  91. infrahub_testcontainers/helpers.py +85 -1
  92. infrahub/core/diff/enricher/summary_counts.py +0 -105
  93. infrahub/dependencies/builder/diff/enricher/summary_counts.py +0 -8
  94. infrahub_sdk/ctl/_file.py +0 -13
  95. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/LICENSE.txt +0 -0
  96. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/WHEEL +0 -0
  97. {infrahub_server-1.1.6.dist-info → infrahub_server-1.1.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,267 @@
1
+ from typing import Any
2
+
3
+ from infrahub.core.query import Query, QueryType
4
+ from infrahub.database import InfrahubDatabase
5
+
6
+ from ..model.path import TrackingId
7
+
8
+
9
+ class DiffFieldsSummaryCountsEnricherQuery(Query):
10
+ """Update summary counters for the attributes and relationshipsin in a diff"""
11
+
12
+ name = "diff_fields_summary_count_enricher"
13
+ type = QueryType.WRITE
14
+ insert_return = False
15
+
16
+ def __init__(
17
+ self,
18
+ diff_branch_name: str,
19
+ tracking_id: TrackingId | None = None,
20
+ diff_id: str | None = None,
21
+ node_uuids: list[str] | None = None,
22
+ **kwargs: Any,
23
+ ) -> None:
24
+ super().__init__(**kwargs)
25
+ if (diff_id is None and tracking_id is None) or (diff_id and tracking_id):
26
+ raise ValueError(
27
+ "DiffFieldsSummaryCountsEnricherQuery requires one and only one of `tracking_id` or `diff_id`"
28
+ )
29
+ self.diff_branch_name = diff_branch_name
30
+ self.tracking_id = tracking_id
31
+ self.diff_id = diff_id
32
+ if self.tracking_id is None and self.diff_id is None:
33
+ raise RuntimeError("tracking_id or diff_id is required")
34
+ self.node_uuids = node_uuids
35
+
36
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
37
+ self.params = {
38
+ "diff_branch_name": self.diff_branch_name,
39
+ "diff_id": self.diff_id,
40
+ "tracking_id": self.tracking_id.serialize() if self.tracking_id else None,
41
+ "node_uuids": self.node_uuids,
42
+ }
43
+ query = """
44
+ MATCH (root:DiffRoot)
45
+ WHERE ($diff_id IS NOT NULL AND root.uuid = $diff_id)
46
+ OR ($tracking_id IS NOT NULL AND root.tracking_id = $tracking_id AND root.diff_branch = $diff_branch_name)
47
+ MATCH (root)-[:DIFF_HAS_NODE]->(dn:DiffNode)
48
+ WHERE $node_uuids IS NULL OR dn.uuid IN $node_uuids
49
+ CALL {
50
+ // ----------------------
51
+ // handle attribute count updates
52
+ // ----------------------
53
+ WITH dn
54
+ MATCH (dn)-[:DIFF_HAS_ATTRIBUTE]->(da:DiffAttribute)
55
+ CALL {
56
+ WITH da
57
+ OPTIONAL MATCH (da)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty)-[:DIFF_HAS_CONFLICT]->(dc:DiffConflict)
58
+ WITH da, count(dc) AS num_conflicts
59
+ SET da.num_conflicts = num_conflicts
60
+ SET da.contains_conflict = (num_conflicts > 0)
61
+ }
62
+ CALL {
63
+ WITH da
64
+ OPTIONAL MATCH (da)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty {action: "added"})
65
+ WITH da, count(dp.action) AS num_added
66
+ SET da.num_added = num_added
67
+ }
68
+ CALL {
69
+ WITH da
70
+ OPTIONAL MATCH (da)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty {action: "updated"})
71
+ WITH da, count(dp.action) AS num_updated
72
+ SET da.num_updated = num_updated
73
+ }
74
+ CALL {
75
+ WITH da
76
+ OPTIONAL MATCH (da)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty {action: "removed"})
77
+ WITH da, count(dp.action) AS num_removed
78
+ SET da.num_removed = num_removed
79
+ }
80
+ }
81
+ CALL {
82
+ WITH dn
83
+ MATCH (dn)-[:DIFF_HAS_RELATIONSHIP]->(dr:DiffRelationship)
84
+ CALL {
85
+ // ----------------------
86
+ // handle relationship element count updates
87
+ // ----------------------
88
+ WITH dr
89
+ MATCH (dr)-[:DIFF_HAS_ELEMENT]->(dre:DiffRelationshipElement)
90
+ CALL {
91
+ WITH dre
92
+ OPTIONAL MATCH (dre)-[*..4]->(dc:DiffConflict)
93
+ WITH dre, count(dc) AS num_conflicts
94
+ SET dre.num_conflicts = num_conflicts
95
+ SET dre.contains_conflict = (num_conflicts > 0)
96
+ }
97
+ CALL {
98
+ WITH dre
99
+ OPTIONAL MATCH (dre)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty {action: "added"})
100
+ WITH dre, count(dp.action) AS num_added
101
+ SET dre.num_added = num_added
102
+ }
103
+ CALL {
104
+ WITH dre
105
+ OPTIONAL MATCH (dre)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty {action: "updated"})
106
+ WITH dre, count(dp.action) AS num_updated
107
+ SET dre.num_updated = num_updated
108
+ }
109
+ CALL {
110
+ WITH dre
111
+ OPTIONAL MATCH (dre)-[:DIFF_HAS_PROPERTY]->(dp:DiffProperty {action: "removed"})
112
+ WITH dre, count(dp.action) AS num_removed
113
+ SET dre.num_removed = num_removed
114
+ }
115
+ }
116
+ // ----------------------
117
+ // handle relationship count updates
118
+ // ----------------------
119
+ OPTIONAL MATCH (dr)-[:DIFF_HAS_ELEMENT]->(conflict_dre:DiffRelationshipElement {contains_conflict: TRUE})
120
+ WITH dr, sum(conflict_dre.num_conflicts) AS num_conflicts
121
+ SET dr.num_conflicts = num_conflicts
122
+ SET dr.contains_conflict = (num_conflicts > 0)
123
+ WITH dr
124
+ CALL {
125
+ WITH dr
126
+ OPTIONAL MATCH (dr)-[:DIFF_HAS_ELEMENT]->(dre:DiffRelationshipElement {action: "added"})
127
+ WITH dr, count(dre.action) AS num_added
128
+ SET dr.num_added = num_added
129
+ }
130
+ CALL {
131
+ WITH dr
132
+ OPTIONAL MATCH (dr)-[:DIFF_HAS_ELEMENT]->(dre:DiffRelationshipElement {action: "updated"})
133
+ WITH dr, count(dre.action) AS num_updated
134
+ SET dr.num_updated = num_updated
135
+ }
136
+ CALL {
137
+ WITH dr
138
+ OPTIONAL MATCH (dr)-[:DIFF_HAS_ELEMENT]->(dre:DiffRelationshipElement {action: "removed"})
139
+ WITH dr, count(dre.action) AS num_removed
140
+ SET dr.num_removed = num_removed
141
+ }
142
+ }
143
+ """
144
+ self.add_to_query(query)
145
+
146
+
147
+ class DiffNodesSummaryCountsEnricherQuery(Query):
148
+ """Update summary counters for the nodes and root in a diff"""
149
+
150
+ name = "diff_nodes_summary_count_enricher"
151
+ type = QueryType.WRITE
152
+ insert_return = False
153
+
154
+ def __init__(
155
+ self,
156
+ diff_branch_name: str,
157
+ tracking_id: TrackingId | None = None,
158
+ diff_id: str | None = None,
159
+ node_uuids: list[str] | None = None,
160
+ **kwargs: Any,
161
+ ) -> None:
162
+ super().__init__(**kwargs)
163
+ if (diff_id is None and tracking_id is None) or (diff_id and tracking_id):
164
+ raise ValueError(
165
+ "DiffNodesSummaryCountsEnricherQuery requires one and only one of `tracking_id` or `diff_id`"
166
+ )
167
+ self.diff_branch_name = diff_branch_name
168
+ self.tracking_id = tracking_id
169
+ self.diff_id = diff_id
170
+ if self.tracking_id is None and self.diff_id is None:
171
+ raise RuntimeError("tracking_id or diff_id is required")
172
+ self.node_uuids = node_uuids
173
+
174
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None:
175
+ self.params = {
176
+ "diff_branch_name": self.diff_branch_name,
177
+ "diff_id": self.diff_id,
178
+ "tracking_id": self.tracking_id.serialize() if self.tracking_id else None,
179
+ "node_uuids": self.node_uuids,
180
+ }
181
+
182
+ query = """
183
+ MATCH (root:DiffRoot)
184
+ WHERE ($diff_id IS NOT NULL AND root.uuid = $diff_id)
185
+ OR ($tracking_id IS NOT NULL AND root.tracking_id = $tracking_id AND root.diff_branch = $diff_branch_name)
186
+ MATCH (root)-[:DIFF_HAS_NODE]->(dn:DiffNode)
187
+ WHERE $node_uuids IS NULL OR dn.uuid IN $node_uuids
188
+ // ----------------------
189
+ // handle node count updates
190
+ // ----------------------
191
+ WITH root, dn, coalesce(dn.num_conflicts, 0) AS previous_num_conflicts
192
+ CALL {
193
+ // ----------------------
194
+ // handle node num_conflicts update
195
+ // ----------------------
196
+ WITH dn
197
+ OPTIONAL MATCH (dn)-[:DIFF_HAS_ATTRIBUTE]->(da:DiffAttribute {contains_conflict: TRUE})
198
+ RETURN sum(da.num_conflicts) AS num_conflicts
199
+ UNION ALL
200
+ WITH dn
201
+ OPTIONAL MATCH (dn)-[:DIFF_HAS_RELATIONSHIP]->(dr:DiffRelationship {contains_conflict: TRUE})
202
+ RETURN sum(dr.num_conflicts) AS num_conflicts
203
+ UNION ALL
204
+ WITH dn
205
+ OPTIONAL MATCH (dn)-[:DIFF_HAS_CONFLICT]->(dc:DiffConflict)
206
+ RETURN count(dc) AS num_conflicts
207
+ }
208
+ WITH root, dn, previous_num_conflicts, sum(num_conflicts) AS updated_num_conflicts
209
+ SET dn.num_conflicts = updated_num_conflicts
210
+ SET dn.contains_conflict = (updated_num_conflicts > 0)
211
+ WITH root, dn, updated_num_conflicts - previous_num_conflicts AS num_conflicts_delta
212
+ CALL {
213
+ // ----------------------
214
+ // handle node added/updated/removed updates
215
+ // ----------------------
216
+ WITH dn
217
+ OPTIONAL MATCH (dn)-[:DIFF_HAS_ATTRIBUTE]->(da:DiffAttribute)
218
+ WITH dn, collect(da.action) AS attr_actions
219
+ OPTIONAL MATCH (dn)-[:DIFF_HAS_RELATIONSHIP]->(dr:DiffRelationship)
220
+ WITH dn, attr_actions, collect(dr.action) AS rel_actions
221
+ WITH dn, attr_actions + rel_actions AS actions
222
+ WITH dn, reduce(counts = [0,0,0], a IN actions |
223
+ CASE
224
+ WHEN a = "added" THEN [counts[0] + 1, counts[1], counts[2]]
225
+ WHEN a = "updated" THEN [counts[0], counts[1] + 1, counts[2]]
226
+ WHEN a = "removed" THEN [counts[0], counts[1], counts[2] + 1]
227
+ ELSE counts
228
+ END
229
+ ) AS action_counts
230
+ WITH dn, action_counts[0] AS num_added, action_counts[1] AS num_updated, action_counts[2] AS num_removed
231
+ SET dn.num_added = num_added
232
+ SET dn.num_updated = num_updated
233
+ SET dn.num_removed = num_removed
234
+ }
235
+ // ----------------------
236
+ // handle conflict updates for parent nodes
237
+ // ----------------------
238
+ WITH root, dn, num_conflicts_delta
239
+ CALL {
240
+ WITH dn, num_conflicts_delta
241
+ OPTIONAL MATCH (dn)-[:DIFF_HAS_RELATIONSHIP|DIFF_HAS_NODE*1..]->(parent_node:DiffNode)
242
+ SET parent_node.num_conflicts = parent_node.num_conflicts + num_conflicts_delta
243
+ SET parent_node.contains_conflict = (parent_node.num_conflicts > 0)
244
+ }
245
+ // ----------------------
246
+ // handle root count updates
247
+ // ----------------------
248
+ WITH root, sum(num_conflicts_delta) AS total_conflicts_delta
249
+ CALL {
250
+ WITH root, total_conflicts_delta
251
+ SET root.num_conflicts = coalesce(root.num_conflicts, 0) + total_conflicts_delta
252
+ SET root.contains_conflict = root.num_conflicts > 0
253
+ WITH root
254
+ OPTIONAL MATCH (root)-[:DIFF_HAS_NODE]->(dn:DiffNode {action: "added"})
255
+ WITH root, count(dn.action) AS num_added
256
+ SET root.num_added = num_added
257
+ WITH root
258
+ OPTIONAL MATCH (root)-[:DIFF_HAS_NODE]->(dn:DiffNode {action: "updated"})
259
+ WITH root, count(dn.action) AS num_updated
260
+ SET root.num_updated = num_updated
261
+ WITH root
262
+ OPTIONAL MATCH (root)-[:DIFF_HAS_NODE]->(dn:DiffNode {action: "removed"})
263
+ WITH root, count(dn.action) AS num_removed
264
+ SET root.num_removed = num_removed
265
+ }
266
+ """
267
+ self.add_to_query(query)
@@ -41,7 +41,8 @@ class EnrichedDiffTimeRangeQuery(Query):
41
41
  query = """
42
42
  // get the roots of all diffs in the query
43
43
  MATCH (diff_root:DiffRoot)
44
- WHERE diff_root.base_branch = $base_branch
44
+ WHERE (diff_root.is_merged IS NULL OR diff_root.is_merged <> TRUE)
45
+ AND diff_root.base_branch = $base_branch
45
46
  AND diff_root.diff_branch = $diff_branch
46
47
  AND diff_root.from_time >= $from_time
47
48
  AND diff_root.to_time <= $to_time
@@ -28,7 +28,6 @@ from .model.path import (
28
28
  if TYPE_CHECKING:
29
29
  from infrahub.core.branch import Branch
30
30
  from infrahub.core.query import QueryResult
31
- from infrahub.core.schema import MainSchemaTypes
32
31
  from infrahub.core.schema.manager import SchemaManager
33
32
  from infrahub.core.schema.relationship_schema import RelationshipSchema
34
33
 
@@ -397,8 +396,12 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
397
396
  force_action: DiffAction | None
398
397
  uuid: str
399
398
  kind: str
399
+ db_id: str
400
+ from_time: Timestamp
401
+ status: RelationshipStatus
400
402
  attributes_by_name: dict[str, DiffAttributeIntermediate] = field(default_factory=dict)
401
- relationships_by_name: dict[str, DiffRelationshipIntermediate] = field(default_factory=dict)
403
+ # {(name, identifier): DiffRelationshipIntermediate}
404
+ relationships_by_identifier: dict[tuple[str, str], DiffRelationshipIntermediate] = field(default_factory=dict)
402
405
 
403
406
  def to_diff_node(self, from_time: Timestamp, include_unchanged: bool) -> DiffNode:
404
407
  attributes = []
@@ -408,7 +411,7 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
408
411
  attributes.append(diff_attr)
409
412
  action, changed_at = self.get_action_and_timestamp(from_time=from_time)
410
413
  relationships = []
411
- for rel in self.relationships_by_name.values():
414
+ for rel in self.relationships_by_identifier.values():
412
415
  diff_rel = rel.to_diff_relationship(include_unchanged=include_unchanged)
413
416
  if include_unchanged or diff_rel.action is not DiffAction.UNCHANGED:
414
417
  relationships.append(diff_rel)
@@ -431,7 +434,7 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
431
434
 
432
435
  @property
433
436
  def is_empty(self) -> bool:
434
- return len(self.attributes_by_name) == 0 and len(self.relationships_by_name) == 0
437
+ return len(self.attributes_by_name) == 0 and len(self.relationships_by_identifier) == 0
435
438
 
436
439
 
437
440
  @dataclass
@@ -495,7 +498,7 @@ class DiffQueryParser:
495
498
  for node in diff_root.nodes_by_id.values():
496
499
  for attribute_name in node.attributes_by_name:
497
500
  node_field_specifiers_map[node.uuid].add(attribute_name)
498
- for relationship_diff in node.relationships_by_name.values():
501
+ for relationship_diff in node.relationships_by_identifier.values():
499
502
  node_field_specifiers_map[node.uuid].add(relationship_diff.identifier)
500
503
  return node_field_specifiers_map
501
504
 
@@ -567,35 +570,53 @@ class DiffQueryParser:
567
570
  diff_root.nodes_by_id[node_id] = DiffNodeIntermediate(
568
571
  uuid=node_id,
569
572
  kind=database_path.node_kind,
573
+ db_id=database_path.node_db_id,
574
+ from_time=database_path.node_changed_at,
575
+ status=database_path.node_status,
570
576
  force_action=DiffAction.UPDATED
571
577
  if database_path.node_branch_support is BranchSupportType.AGNOSTIC
572
578
  else None,
573
579
  )
574
580
  diff_node = diff_root.nodes_by_id[node_id]
581
+ # special handling for nodes that have their kind updated, which results in 2 nodes with the same uuid
582
+ if diff_node.db_id != database_path.node_db_id and (
583
+ database_path.node_changed_at > diff_node.from_time
584
+ or (
585
+ database_path.node_changed_at >= diff_node.from_time
586
+ and (diff_node.status, database_path.node_status)
587
+ == (RelationshipStatus.DELETED, RelationshipStatus.ACTIVE)
588
+ )
589
+ ):
590
+ diff_node.kind = database_path.node_kind
591
+ diff_node.db_id = database_path.node_db_id
592
+ diff_node.from_time = database_path.node_changed_at
593
+ diff_node.status = database_path.node_status
575
594
  diff_node.track_database_path(database_path=database_path)
576
595
  return diff_node
577
596
 
578
- def _get_relationship_schema(
579
- self, database_path: DatabasePath, node_schema: MainSchemaTypes
580
- ) -> RelationshipSchema | None:
581
- relationship_schemas = node_schema.get_relationships_by_identifier(id=database_path.attribute_name)
582
- if len(relationship_schemas) == 1:
583
- return relationship_schemas[0]
584
- possible_path_directions = database_path.possible_relationship_directions
585
- for rel_schema in relationship_schemas:
586
- if rel_schema.direction in possible_path_directions:
587
- return rel_schema
597
+ def _get_relationship_schema(self, database_path: DatabasePath) -> RelationshipSchema | None:
598
+ branches_to_check = [database_path.deepest_branch]
599
+ if database_path.deepest_branch == self.diff_branch_name:
600
+ branches_to_check.append(self.base_branch_name)
601
+ for schema_branch_name in branches_to_check:
602
+ node_schema = self.schema_manager.get(
603
+ name=database_path.node_kind, branch=schema_branch_name, duplicate=False
604
+ )
605
+ relationship_schemas = node_schema.get_relationships_by_identifier(id=database_path.attribute_name)
606
+ if len(relationship_schemas) == 1:
607
+ return relationship_schemas[0]
608
+ possible_path_directions = database_path.possible_relationship_directions
609
+ for rel_schema in relationship_schemas:
610
+ if rel_schema.direction in possible_path_directions:
611
+ return rel_schema
588
612
  return None
589
613
 
590
614
  def _update_attribute_level(self, database_path: DatabasePath, diff_node: DiffNodeIntermediate) -> None:
591
- node_schema = self.schema_manager.get(
592
- name=database_path.node_kind, branch=database_path.deepest_branch, duplicate=False
593
- )
594
615
  if "Attribute" in database_path.attribute_node.labels:
595
616
  diff_attribute = self._get_diff_attribute(database_path=database_path, diff_node=diff_node)
596
617
  self._update_attribute_property(database_path=database_path, diff_attribute=diff_attribute)
597
618
  return
598
- relationship_schema = self._get_relationship_schema(database_path=database_path, node_schema=node_schema)
619
+ relationship_schema = self._get_relationship_schema(database_path=database_path)
599
620
  if not relationship_schema:
600
621
  return
601
622
  diff_relationship = self._get_diff_relationship(
@@ -649,7 +670,9 @@ class DiffQueryParser:
649
670
  relationship_schema: RelationshipSchema,
650
671
  database_path: DatabasePath,
651
672
  ) -> DiffRelationshipIntermediate:
652
- diff_relationship = diff_node.relationships_by_name.get(relationship_schema.name)
673
+ diff_relationship = diff_node.relationships_by_identifier.get(
674
+ (relationship_schema.name, relationship_schema.get_identifier())
675
+ )
653
676
  if not diff_relationship:
654
677
  branch_name = database_path.deepest_branch
655
678
  from_time = self.from_time
@@ -663,7 +686,9 @@ class DiffQueryParser:
663
686
  identifier=relationship_schema.get_identifier(),
664
687
  from_time=from_time,
665
688
  )
666
- diff_node.relationships_by_name[relationship_schema.name] = diff_relationship
689
+ diff_node.relationships_by_identifier[relationship_schema.name, relationship_schema.get_identifier()] = (
690
+ diff_relationship
691
+ )
667
692
  return diff_relationship
668
693
 
669
694
  def _apply_base_branch_previous_values(self) -> None:
@@ -700,8 +725,8 @@ class DiffQueryParser:
700
725
  def _apply_relationship_previous_values(
701
726
  self, diff_node: DiffNodeIntermediate, base_diff_node: DiffNodeIntermediate
702
727
  ) -> None:
703
- for relationship_name, diff_relationship in diff_node.relationships_by_name.items():
704
- base_diff_relationship = base_diff_node.relationships_by_name.get(relationship_name)
728
+ for relationship_key, diff_relationship in diff_node.relationships_by_identifier.items():
729
+ base_diff_relationship = base_diff_node.relationships_by_identifier.get(relationship_key)
705
730
  if not base_diff_relationship:
706
731
  continue
707
732
  for db_id, property_set in diff_relationship.properties_by_db_id.items():
@@ -754,7 +779,7 @@ class DiffQueryParser:
754
779
  continue
755
780
  if ordered_diff_values[-1].changed_at >= self.diff_branched_from_time:
756
781
  return
757
- for relationship_diff in node_diff.relationships_by_name.values():
782
+ for relationship_diff in node_diff.relationships_by_identifier.values():
758
783
  for diff_relationship_property_list in relationship_diff.properties_by_db_id.values():
759
784
  for diff_relationship_property in diff_relationship_property_list:
760
785
  if diff_relationship_property.changed_at >= self.diff_branched_from_time:
@@ -18,10 +18,12 @@ from ..model.path import (
18
18
  EnrichedDiffSingleRelationship,
19
19
  deserialize_tracking_id,
20
20
  )
21
+ from ..parent_node_adder import DiffParentNodeAdder, ParentNodeAddRequest
21
22
 
22
23
 
23
24
  class EnrichedDiffDeserializer:
24
- def __init__(self) -> None:
25
+ def __init__(self, parent_adder: DiffParentNodeAdder) -> None:
26
+ self.parent_adder = parent_adder
25
27
  self._diff_root_map: dict[str, EnrichedDiffRoot] = {}
26
28
  self._diff_node_map: dict[tuple[str, str], EnrichedDiffNode] = {}
27
29
  self._diff_node_attr_map: dict[tuple[str, str, str], EnrichedDiffAttribute] = {}
@@ -127,6 +129,7 @@ class EnrichedDiffDeserializer:
127
129
 
128
130
  def _deserialize_parents(self) -> None:
129
131
  for enriched_root, node_path_tuples in self._parents_path_map.items():
132
+ self.parent_adder.initialize(enriched_diff_root=enriched_root)
130
133
  for node_uuid, parents_path in node_path_tuples:
131
134
  # Remove the node itself from the path
132
135
  parents_path_slice = parents_path.nodes[1:]
@@ -134,7 +137,7 @@ class EnrichedDiffDeserializer:
134
137
  # TODO Ensure the list is even
135
138
  current_node_uuid = node_uuid
136
139
  for rel, parent in zip(parents_path_slice[::2], parents_path_slice[1::2]):
137
- enriched_root.add_parent(
140
+ parent_request = ParentNodeAddRequest(
138
141
  node_id=current_node_uuid,
139
142
  parent_id=parent.get("uuid"),
140
143
  parent_kind=parent.get("kind"),
@@ -144,6 +147,7 @@ class EnrichedDiffDeserializer:
144
147
  parent_rel_cardinality=RelationshipCardinality(rel.get("cardinality")),
145
148
  parent_rel_label=rel.get("label"),
146
149
  )
150
+ self.parent_adder.add_parent(parent_request=parent_request)
147
151
  current_node_uuid = parent.get("uuid")
148
152
 
149
153
  @classmethod
@@ -164,23 +168,23 @@ class EnrichedDiffDeserializer:
164
168
  def build_diff_root_metadata(cls, root_node: Neo4jNode) -> EnrichedDiffRootMetadata:
165
169
  from_time = Timestamp(str(root_node.get("from_time")))
166
170
  to_time = Timestamp(str(root_node.get("to_time")))
167
- tracking_id_str = cls._get_str_or_none_property_value(node=root_node, property_name="tracking_id")
168
- tracking_id = None
169
- if tracking_id_str:
170
- tracking_id = deserialize_tracking_id(tracking_id_str=tracking_id_str)
171
+ partner_uuid = cls._get_str_or_none_property_value(node=root_node, property_name="partner_uuid")
172
+ tracking_id_str = str(root_node.get("tracking_id"))
173
+ tracking_id = deserialize_tracking_id(tracking_id_str=tracking_id_str)
171
174
  return EnrichedDiffRootMetadata(
172
175
  base_branch_name=str(root_node.get("base_branch")),
173
176
  diff_branch_name=str(root_node.get("diff_branch")),
174
177
  from_time=from_time,
175
178
  to_time=to_time,
176
179
  uuid=str(root_node.get("uuid")),
177
- partner_uuid=str(root_node.get("partner_uuid")),
180
+ partner_uuid=partner_uuid,
178
181
  tracking_id=tracking_id,
179
- num_added=int(root_node.get("num_added")),
180
- num_updated=int(root_node.get("num_updated")),
181
- num_removed=int(root_node.get("num_removed")),
182
- num_conflicts=int(root_node.get("num_conflicts")),
182
+ num_added=int(root_node.get("num_added", 0)),
183
+ num_updated=int(root_node.get("num_updated", 0)),
184
+ num_removed=int(root_node.get("num_removed", 0)),
185
+ num_conflicts=int(root_node.get("num_conflicts", 0)),
183
186
  contains_conflict=str(root_node.get("contains_conflict")).lower() == "true",
187
+ exists_on_database=True,
184
188
  )
185
189
 
186
190
  def _deserialize_diff_node(self, node_node: Neo4jNode, enriched_root: EnrichedDiffRoot) -> EnrichedDiffNode:
@@ -197,10 +201,10 @@ class EnrichedDiffDeserializer:
197
201
  changed_at=Timestamp(timestamp_str) if timestamp_str else None,
198
202
  action=DiffAction(str(node_node.get("action"))),
199
203
  path_identifier=str(node_node.get("path_identifier")),
200
- num_added=int(node_node.get("num_added")),
201
- num_updated=int(node_node.get("num_updated")),
202
- num_removed=int(node_node.get("num_removed")),
203
- num_conflicts=int(node_node.get("num_conflicts")),
204
+ num_added=int(node_node.get("num_added", 0)),
205
+ num_updated=int(node_node.get("num_updated", 0)),
206
+ num_removed=int(node_node.get("num_removed", 0)),
207
+ num_conflicts=int(node_node.get("num_conflicts", 0)),
204
208
  contains_conflict=str(node_node.get("contains_conflict")).lower() == "true",
205
209
  )
206
210
  self._diff_node_map[node_key] = enriched_node
@@ -220,10 +224,10 @@ class EnrichedDiffDeserializer:
220
224
  changed_at=Timestamp(str(diff_attr_node.get("changed_at"))),
221
225
  path_identifier=str(diff_attr_node.get("path_identifier")),
222
226
  action=DiffAction(str(diff_attr_node.get("action"))),
223
- num_added=int(diff_attr_node.get("num_added")),
224
- num_updated=int(diff_attr_node.get("num_updated")),
225
- num_removed=int(diff_attr_node.get("num_removed")),
226
- num_conflicts=int(diff_attr_node.get("num_conflicts")),
227
+ num_added=int(diff_attr_node.get("num_added", 0)),
228
+ num_updated=int(diff_attr_node.get("num_updated", 0)),
229
+ num_removed=int(diff_attr_node.get("num_removed", 0)),
230
+ num_conflicts=int(diff_attr_node.get("num_conflicts", 0)),
227
231
  contains_conflict=str(diff_attr_node.get("contains_conflict")).lower() == "true",
228
232
  )
229
233
  self._diff_node_attr_map[attr_key] = enriched_attr
@@ -247,10 +251,10 @@ class EnrichedDiffDeserializer:
247
251
  changed_at=Timestamp(timestamp_str) if timestamp_str else None,
248
252
  action=DiffAction(str(relationship_group_node.get("action"))),
249
253
  path_identifier=str(relationship_group_node.get("path_identifier")),
250
- num_added=int(relationship_group_node.get("num_added")),
251
- num_conflicts=int(relationship_group_node.get("num_conflicts")),
252
- num_removed=int(relationship_group_node.get("num_removed")),
253
- num_updated=int(relationship_group_node.get("num_updated")),
254
+ num_added=int(relationship_group_node.get("num_added", 0)),
255
+ num_conflicts=int(relationship_group_node.get("num_conflicts", 0)),
256
+ num_removed=int(relationship_group_node.get("num_removed", 0)),
257
+ num_updated=int(relationship_group_node.get("num_updated", 0)),
254
258
  contains_conflict=str(relationship_group_node.get("contains_conflict")).lower() == "true",
255
259
  )
256
260
 
@@ -282,10 +286,10 @@ class EnrichedDiffDeserializer:
282
286
  peer_id=diff_element_peer_id,
283
287
  peer_label=peer_label,
284
288
  path_identifier=str(relationship_element_node.get("path_identifier")),
285
- num_added=int(relationship_element_node.get("num_added")),
286
- num_updated=int(relationship_element_node.get("num_updated")),
287
- num_removed=int(relationship_element_node.get("num_removed")),
288
- num_conflicts=int(relationship_element_node.get("num_conflicts")),
289
+ num_added=int(relationship_element_node.get("num_added", 0)),
290
+ num_updated=int(relationship_element_node.get("num_updated", 0)),
291
+ num_removed=int(relationship_element_node.get("num_removed", 0)),
292
+ num_conflicts=int(relationship_element_node.get("num_conflicts", 0)),
289
293
  contains_conflict=str(relationship_element_node.get("contains_conflict")).lower() == "true",
290
294
  )
291
295
  enriched_relationship_group.relationships.add(enriched_rel_element)