infrahub-server 1.2.9rc0__py3-none-any.whl → 1.3.0a0__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 (166) hide show
  1. infrahub/actions/constants.py +86 -0
  2. infrahub/actions/gather.py +114 -0
  3. infrahub/actions/models.py +241 -0
  4. infrahub/actions/parsers.py +104 -0
  5. infrahub/actions/schema.py +382 -0
  6. infrahub/actions/tasks.py +126 -0
  7. infrahub/actions/triggers.py +21 -0
  8. infrahub/cli/db.py +1 -2
  9. infrahub/computed_attribute/models.py +13 -0
  10. infrahub/computed_attribute/tasks.py +48 -26
  11. infrahub/config.py +9 -0
  12. infrahub/core/account.py +24 -47
  13. infrahub/core/attribute.py +53 -14
  14. infrahub/core/branch/models.py +8 -9
  15. infrahub/core/branch/tasks.py +0 -2
  16. infrahub/core/constants/infrahubkind.py +8 -0
  17. infrahub/core/constraint/node/runner.py +1 -1
  18. infrahub/core/convert_object_type/__init__.py +0 -0
  19. infrahub/core/convert_object_type/conversion.py +122 -0
  20. infrahub/core/convert_object_type/schema_mapping.py +56 -0
  21. infrahub/core/diff/calculator.py +65 -11
  22. infrahub/core/diff/combiner.py +38 -31
  23. infrahub/core/diff/coordinator.py +44 -28
  24. infrahub/core/diff/data_check_synchronizer.py +3 -2
  25. infrahub/core/diff/enricher/hierarchy.py +36 -27
  26. infrahub/core/diff/ipam_diff_parser.py +5 -4
  27. infrahub/core/diff/merger/merger.py +46 -16
  28. infrahub/core/diff/merger/serializer.py +1 -0
  29. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  30. infrahub/core/diff/model/path.py +58 -58
  31. infrahub/core/diff/parent_node_adder.py +14 -16
  32. infrahub/core/diff/query/all_conflicts.py +1 -5
  33. infrahub/core/diff/query/artifact.py +10 -20
  34. infrahub/core/diff/query/diff_get.py +3 -6
  35. infrahub/core/diff/query/drop_nodes.py +42 -0
  36. infrahub/core/diff/query/field_specifiers.py +8 -7
  37. infrahub/core/diff/query/field_summary.py +2 -4
  38. infrahub/core/diff/query/filters.py +15 -1
  39. infrahub/core/diff/query/merge.py +284 -101
  40. infrahub/core/diff/query/save.py +26 -34
  41. infrahub/core/diff/query/summary_counts_enricher.py +34 -54
  42. infrahub/core/diff/query_parser.py +55 -65
  43. infrahub/core/diff/repository/deserializer.py +38 -24
  44. infrahub/core/diff/repository/repository.py +31 -12
  45. infrahub/core/diff/tasks.py +3 -3
  46. infrahub/core/graph/__init__.py +1 -1
  47. infrahub/core/manager.py +14 -11
  48. infrahub/core/migrations/graph/__init__.py +2 -0
  49. infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
  50. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
  51. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
  52. infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
  53. infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
  54. infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
  55. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  56. infrahub/core/migrations/graph/m028_delete_diffs.py +38 -0
  57. infrahub/core/migrations/query/attribute_add.py +1 -2
  58. infrahub/core/migrations/query/attribute_rename.py +3 -6
  59. infrahub/core/migrations/query/delete_element_in_schema.py +3 -6
  60. infrahub/core/migrations/query/node_duplicate.py +3 -6
  61. infrahub/core/migrations/query/relationship_duplicate.py +3 -6
  62. infrahub/core/migrations/schema/node_attribute_remove.py +3 -6
  63. infrahub/core/migrations/schema/node_remove.py +3 -6
  64. infrahub/core/models.py +29 -2
  65. infrahub/core/node/__init__.py +18 -4
  66. infrahub/core/node/create.py +211 -0
  67. infrahub/core/protocols.py +51 -0
  68. infrahub/core/protocols_base.py +3 -0
  69. infrahub/core/query/__init__.py +2 -2
  70. infrahub/core/query/branch.py +27 -17
  71. infrahub/core/query/diff.py +186 -81
  72. infrahub/core/query/ipam.py +10 -20
  73. infrahub/core/query/node.py +65 -49
  74. infrahub/core/query/relationship.py +156 -58
  75. infrahub/core/query/resource_manager.py +1 -2
  76. infrahub/core/query/subquery.py +4 -6
  77. infrahub/core/relationship/model.py +4 -1
  78. infrahub/core/schema/__init__.py +2 -1
  79. infrahub/core/schema/attribute_parameters.py +36 -0
  80. infrahub/core/schema/attribute_schema.py +83 -8
  81. infrahub/core/schema/basenode_schema.py +25 -1
  82. infrahub/core/schema/definitions/core/__init__.py +21 -0
  83. infrahub/core/schema/definitions/internal.py +13 -3
  84. infrahub/core/schema/generated/attribute_schema.py +9 -3
  85. infrahub/core/schema/schema_branch.py +15 -7
  86. infrahub/core/validators/__init__.py +5 -1
  87. infrahub/core/validators/attribute/choices.py +1 -2
  88. infrahub/core/validators/attribute/enum.py +1 -2
  89. infrahub/core/validators/attribute/kind.py +1 -2
  90. infrahub/core/validators/attribute/length.py +13 -6
  91. infrahub/core/validators/attribute/optional.py +1 -2
  92. infrahub/core/validators/attribute/regex.py +5 -5
  93. infrahub/core/validators/attribute/unique.py +1 -3
  94. infrahub/core/validators/determiner.py +18 -2
  95. infrahub/core/validators/enum.py +7 -0
  96. infrahub/core/validators/node/hierarchy.py +3 -6
  97. infrahub/core/validators/query.py +1 -3
  98. infrahub/core/validators/relationship/count.py +6 -12
  99. infrahub/core/validators/relationship/optional.py +2 -4
  100. infrahub/core/validators/relationship/peer.py +3 -8
  101. infrahub/core/validators/tasks.py +1 -1
  102. infrahub/core/validators/uniqueness/query.py +12 -9
  103. infrahub/database/__init__.py +1 -3
  104. infrahub/events/group_action.py +1 -0
  105. infrahub/graphql/analyzer.py +139 -18
  106. infrahub/graphql/app.py +1 -1
  107. infrahub/graphql/loaders/node.py +1 -1
  108. infrahub/graphql/loaders/peers.py +1 -1
  109. infrahub/graphql/manager.py +4 -0
  110. infrahub/graphql/mutations/action.py +164 -0
  111. infrahub/graphql/mutations/convert_object_type.py +62 -0
  112. infrahub/graphql/mutations/main.py +24 -175
  113. infrahub/graphql/mutations/proposed_change.py +21 -18
  114. infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
  115. infrahub/graphql/queries/diff/tree.py +2 -1
  116. infrahub/graphql/queries/relationship.py +1 -1
  117. infrahub/graphql/resolvers/many_relationship.py +4 -4
  118. infrahub/graphql/resolvers/resolver.py +4 -4
  119. infrahub/graphql/resolvers/single_relationship.py +2 -2
  120. infrahub/graphql/schema.py +6 -0
  121. infrahub/graphql/subscription/graphql_query.py +2 -2
  122. infrahub/graphql/types/branch.py +1 -1
  123. infrahub/menu/menu.py +31 -0
  124. infrahub/message_bus/messages/__init__.py +0 -10
  125. infrahub/message_bus/operations/__init__.py +0 -8
  126. infrahub/message_bus/operations/refresh/registry.py +1 -1
  127. infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
  128. infrahub/patch/queries/delete_duplicated_edges.py +5 -10
  129. infrahub/prefect_server/models.py +1 -19
  130. infrahub/proposed_change/models.py +68 -3
  131. infrahub/proposed_change/tasks.py +907 -30
  132. infrahub/task_manager/models.py +10 -6
  133. infrahub/telemetry/database.py +1 -1
  134. infrahub/telemetry/tasks.py +1 -1
  135. infrahub/trigger/catalogue.py +2 -0
  136. infrahub/trigger/models.py +29 -3
  137. infrahub/trigger/setup.py +51 -15
  138. infrahub/trigger/tasks.py +4 -5
  139. infrahub/types.py +1 -1
  140. infrahub/webhook/models.py +2 -1
  141. infrahub/workflows/catalogue.py +85 -0
  142. infrahub/workflows/initialization.py +1 -3
  143. infrahub_sdk/timestamp.py +2 -2
  144. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/METADATA +4 -4
  145. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/RECORD +153 -146
  146. infrahub_testcontainers/container.py +0 -1
  147. infrahub_testcontainers/docker-compose.test.yml +4 -4
  148. infrahub_testcontainers/helpers.py +8 -2
  149. infrahub_testcontainers/performance_test.py +6 -3
  150. infrahub/message_bus/messages/check_generator_run.py +0 -26
  151. infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
  152. infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
  153. infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
  154. infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
  155. infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
  156. infrahub/message_bus/operations/check/__init__.py +0 -3
  157. infrahub/message_bus/operations/check/generator.py +0 -156
  158. infrahub/message_bus/operations/finalize/__init__.py +0 -3
  159. infrahub/message_bus/operations/finalize/validator.py +0 -133
  160. infrahub/message_bus/operations/requests/__init__.py +0 -9
  161. infrahub/message_bus/operations/requests/generator_definition.py +0 -140
  162. infrahub/message_bus/operations/requests/proposed_change.py +0 -629
  163. /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
  164. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/LICENSE.txt +0 -0
  165. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/WHEEL +0 -0
  166. {infrahub_server-1.2.9rc0.dist-info → infrahub_server-1.3.0a0.dist-info}/entry_points.txt +0 -0
@@ -20,6 +20,7 @@ class DiffMergeQuery(Query):
20
20
  node_diff_dicts: dict[str, Any],
21
21
  at: Timestamp,
22
22
  target_branch: Branch,
23
+ migrated_kinds_id_map: dict[str, str],
23
24
  **kwargs: Any,
24
25
  ) -> None:
25
26
  super().__init__(**kwargs)
@@ -27,6 +28,7 @@ class DiffMergeQuery(Query):
27
28
  self.at = at
28
29
  self.target_branch = target_branch
29
30
  self.source_branch_name = self.branch.name
31
+ self.migrated_kinds_id_map = migrated_kinds_id_map
30
32
 
31
33
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
32
34
  self.params = {
@@ -35,37 +37,45 @@ class DiffMergeQuery(Query):
35
37
  "branch_level": self.target_branch.hierarchy_level,
36
38
  "target_branch": self.target_branch.name,
37
39
  "source_branch": self.source_branch_name,
40
+ "migrated_kinds_id_map": self.migrated_kinds_id_map,
41
+ "migrated_kinds_uuids": list(self.migrated_kinds_id_map.keys()),
38
42
  }
39
43
  # ruff: noqa: E501
40
44
  query = """
41
45
  UNWIND $node_diff_dicts AS node_diff_map
42
- CALL {
43
- WITH node_diff_map
46
+ WITH node_diff_map, node_diff_map.uuid IN $migrated_kinds_uuids AS is_node_kind_migration
47
+ WITH node_diff_map, is_node_kind_migration, CASE
48
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
49
+ WHEN is_node_kind_migration THEN $migrated_kinds_id_map[node_diff_map.uuid]
50
+ ELSE NULL
51
+ END AS node_db_id
52
+ CALL (node_diff_map, node_db_id) {
44
53
  MATCH (n:Node {uuid: node_diff_map.uuid})
54
+ WHERE node_db_id IS NULL
55
+ OR %(id_func)s(n) = node_db_id
45
56
  RETURN n
46
57
  }
47
- WITH n, node_diff_map
48
- CALL {
49
- WITH n, node_diff_map
50
- WITH n, node_diff_map, CASE
58
+ WITH n, node_diff_map, is_node_kind_migration
59
+ CALL (n, node_diff_map, is_node_kind_migration) {
60
+ WITH CASE
51
61
  WHEN node_diff_map.action = "ADDED" THEN "active"
52
62
  WHEN node_diff_map.action = "REMOVED" THEN "deleted"
53
63
  ELSE NULL
54
64
  END AS node_rel_status
55
- CALL {
65
+ CALL (n, node_diff_map, is_node_kind_migration, node_rel_status) {
56
66
  // ------------------------------
57
67
  // only make IS_PART_OF updates if node is ADDED or REMOVED
58
68
  // ------------------------------
59
- WITH n, node_diff_map, node_rel_status
60
- WITH n, node_diff_map, node_rel_status
69
+ WITH node_rel_status
61
70
  WHERE node_rel_status IS NOT NULL
71
+ // nodes with a migrated kind are handled in DiffMergeMigratedKindsQuery
72
+ AND is_node_kind_migration = FALSE
62
73
  MATCH (root:Root)
63
74
  // ------------------------------
64
75
  // set IS_PART_OF.to, optionally, target branch
65
76
  // ------------------------------
66
77
  WITH root, n, node_rel_status
67
- CALL {
68
- WITH root, n, node_rel_status
78
+ CALL (root, n, node_rel_status) {
69
79
  OPTIONAL MATCH (root)<-[target_r_root:IS_PART_OF {branch: $target_branch, status: "active"}]-(n)
70
80
  WHERE node_rel_status = "deleted"
71
81
  AND target_r_root.from <= $at AND target_r_root.to IS NULL
@@ -75,13 +85,12 @@ CALL {
75
85
  // create new IS_PART_OF relationship on target_branch
76
86
  // ------------------------------
77
87
  WITH root, n, node_rel_status
78
- CALL {
79
- WITH root, n, node_rel_status
88
+ CALL (root, n, node_rel_status) {
80
89
  OPTIONAL MATCH (root)<-[r_root:IS_PART_OF {branch: $target_branch}]-(n)
81
90
  WHERE r_root.status = node_rel_status
82
91
  AND r_root.from <= $at
83
92
  AND (r_root.to >= $at OR r_root.to IS NULL)
84
- WITH root, r_root, n, node_rel_status
93
+ WITH r_root
85
94
  WHERE r_root IS NULL
86
95
  CREATE (root)
87
96
  <-[:IS_PART_OF { branch: $target_branch, branch_level: $branch_level, from: $at, status: node_rel_status }]
@@ -90,12 +99,10 @@ CALL {
90
99
  // ------------------------------
91
100
  // shortcut to delete all attributes and relationships for this node if the node is deleted
92
101
  // ------------------------------
93
- CALL {
94
- WITH n, node_rel_status
102
+ CALL (n, node_rel_status) {
95
103
  WITH n, node_rel_status
96
104
  WHERE node_rel_status = "deleted"
97
- CALL {
98
- WITH n
105
+ CALL (n) {
99
106
  OPTIONAL MATCH (n)-[rel1:IS_RELATED]-(:Relationship)-[rel2]-(p)
100
107
  WHERE (p.uuid IS NULL OR n.uuid <> p.uuid)
101
108
  AND rel1.branch = $target_branch
@@ -104,7 +111,6 @@ CALL {
104
111
  AND rel2.status = "active"
105
112
  RETURN rel1, rel2
106
113
  UNION
107
- WITH n
108
114
  OPTIONAL MATCH (n)-[rel1:HAS_ATTRIBUTE]->(:Attribute)-[rel2]->()
109
115
  WHERE type(rel2) <> "HAS_ATTRIBUTE"
110
116
  AND rel1.branch = $target_branch
@@ -113,7 +119,7 @@ CALL {
113
119
  AND rel2.status = "active"
114
120
  RETURN rel1, rel2
115
121
  }
116
- WITH n, rel1, rel2
122
+ WITH rel1, rel2
117
123
  WHERE rel1.to IS NULL
118
124
  AND rel2.to IS NULL
119
125
  AND rel1.from <= $at
@@ -124,10 +130,8 @@ CALL {
124
130
  // and delete HAS_OWNER and HAS_SOURCE edges to this node if the node is deleted
125
131
  // ------------------------------
126
132
  WITH n
127
- CALL {
128
- WITH n
129
- CALL {
130
- WITH n
133
+ CALL (n) {
134
+ CALL (n) {
131
135
  MATCH (n)<-[rel:HAS_OWNER]-()
132
136
  WHERE rel.branch = $target_branch
133
137
  AND rel.status = "active"
@@ -147,9 +151,8 @@ CALL {
147
151
  }
148
152
  }
149
153
  WITH n, node_diff_map
150
- CALL {
151
- WITH n, node_diff_map
152
- WITH n, CASE
154
+ CALL (n, node_diff_map) {
155
+ WITH CASE
153
156
  WHEN node_diff_map.attributes IS NULL OR node_diff_map.attributes = [] THEN [NULL]
154
157
  ELSE node_diff_map.attributes
155
158
  END AS attribute_maps
@@ -157,15 +160,13 @@ CALL {
157
160
  // ------------------------------
158
161
  // handle updates for attributes under this node
159
162
  // ------------------------------
160
- CALL {
161
- WITH n, attribute_diff_map
162
- WITH n, attribute_diff_map.name AS attr_name, CASE
163
+ CALL (n, attribute_diff_map) {
164
+ WITH attribute_diff_map.name AS attr_name, CASE
163
165
  WHEN attribute_diff_map.action = "ADDED" THEN "active"
164
166
  WHEN attribute_diff_map.action = "REMOVED" THEN "deleted"
165
167
  ELSE NULL
166
168
  END AS attr_rel_status
167
- CALL {
168
- WITH n, attr_name
169
+ CALL (n, attr_name) {
169
170
  OPTIONAL MATCH (n)-[has_attr:HAS_ATTRIBUTE]->(a:Attribute {name: attr_name})
170
171
  WHERE has_attr.branch IN [$source_branch, $target_branch]
171
172
  RETURN a
@@ -176,8 +177,7 @@ CALL {
176
177
  // ------------------------------
177
178
  // set HAS_ATTRIBUTE.to on target branch if necessary
178
179
  // ------------------------------
179
- CALL {
180
- WITH n, attr_rel_status, a
180
+ CALL (n, attr_rel_status, a) {
181
181
  OPTIONAL MATCH (n)
182
182
  -[target_r_attr:HAS_ATTRIBUTE {branch: $target_branch, status: "active"}]
183
183
  ->(a)
@@ -189,15 +189,14 @@ CALL {
189
189
  // ------------------------------
190
190
  // conditionally create new HAS_ATTRIBUTE relationship on target_branch, if necessary
191
191
  // ------------------------------
192
- CALL {
193
- WITH n, attr_rel_status, a
192
+ CALL (n, attr_rel_status, a) {
194
193
  WITH n, attr_rel_status, a
195
194
  WHERE a IS NOT NULL
196
195
  OPTIONAL MATCH (n)-[r_attr:HAS_ATTRIBUTE {branch: $target_branch}]->(a)
197
196
  WHERE r_attr.status = attr_rel_status
198
197
  AND r_attr.from <= $at
199
198
  AND (r_attr.to >= $at OR r_attr.to IS NULL)
200
- WITH n, r_attr, attr_rel_status, a
199
+ WITH r_attr
201
200
  WHERE r_attr IS NULL
202
201
  CREATE (n)-[:HAS_ATTRIBUTE { branch: $target_branch, branch_level: $branch_level, from: $at, status: attr_rel_status }]->(a)
203
202
  }
@@ -206,34 +205,39 @@ CALL {
206
205
  RETURN 1 AS done
207
206
  }
208
207
  WITH n, node_diff_map
209
- CALL {
210
- WITH n,node_diff_map
208
+ CALL (n, node_diff_map) {
211
209
  UNWIND node_diff_map.relationships AS relationship_diff_map
212
210
  // ------------------------------
213
211
  // handle updates for relationships under this node
214
212
  // ------------------------------
215
- CALL {
216
- WITH n, relationship_diff_map
217
- WITH n, relationship_diff_map.peer_id AS rel_peer_id, relationship_diff_map.name AS rel_name, CASE
218
- WHEN relationship_diff_map.action = "ADDED" THEN "active"
219
- WHEN relationship_diff_map.action = "REMOVED" THEN "deleted"
220
- ELSE NULL
221
- END AS related_rel_status
213
+ CALL (n, relationship_diff_map) {
214
+ WITH
215
+ relationship_diff_map.peer_id AS rel_peer_id, relationship_diff_map.name AS rel_name,
216
+ CASE
217
+ WHEN relationship_diff_map.action = "ADDED" THEN "active"
218
+ WHEN relationship_diff_map.action = "REMOVED" THEN "deleted"
219
+ ELSE NULL
220
+ END AS related_rel_status,
221
+ CASE
222
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
223
+ WHEN relationship_diff_map.peer_id IN $migrated_kinds_uuids THEN $migrated_kinds_id_map[relationship_diff_map.peer_id]
224
+ ELSE NULL
225
+ END AS rel_peer_db_id
222
226
  // ------------------------------
223
227
  // determine the directions of each IS_RELATED
224
228
  // ------------------------------
225
- CALL {
226
- WITH n, rel_name, rel_peer_id, related_rel_status
229
+ CALL (n, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status) {
227
230
  MATCH (n)
228
231
  -[source_r_rel_1:IS_RELATED]
229
232
  -(r:Relationship {name: rel_name})
230
233
  -[source_r_rel_2:IS_RELATED]
231
- -(:Node {uuid: rel_peer_id})
232
- WHERE source_r_rel_1.branch IN [$source_branch, $target_branch]
234
+ -(rel_peer:Node {uuid: rel_peer_id})
235
+ WHERE (rel_peer_db_id IS NULL OR %(id_func)s(rel_peer) = rel_peer_db_id)
236
+ AND source_r_rel_1.branch IN [$source_branch, $target_branch]
233
237
  AND source_r_rel_2.branch IN [$source_branch, $target_branch]
234
238
  AND source_r_rel_1.from <= $at AND source_r_rel_1.to IS NULL
235
239
  AND source_r_rel_2.from <= $at AND source_r_rel_2.to IS NULL
236
- WITH n, rel_name, rel_peer_id, related_rel_status, r, source_r_rel_1, source_r_rel_2
240
+ WITH r, source_r_rel_1, source_r_rel_2
237
241
  ORDER BY source_r_rel_1.branch_level DESC, source_r_rel_2.branch_level DESC, source_r_rel_1.from DESC, source_r_rel_2.from DESC
238
242
  LIMIT 1
239
243
  RETURN r, CASE
@@ -247,27 +251,27 @@ CALL {
247
251
  source_r_rel_1.hierarchy AS r1_hierarchy,
248
252
  source_r_rel_2.hierarchy AS r2_hierarchy
249
253
  }
250
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, related_rel_status
251
- CALL {
252
- WITH n, rel_name, rel_peer_id, related_rel_status
254
+ WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
255
+ CALL (n, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status) {
253
256
  OPTIONAL MATCH (n)
254
257
  -[target_r_rel_1:IS_RELATED {branch: $target_branch, status: "active"}]
255
258
  -(:Relationship {name: rel_name})
256
259
  -[target_r_rel_2:IS_RELATED {branch: $target_branch, status: "active"}]
257
- -(:Node {uuid: rel_peer_id})
260
+ -(rel_peer:Node {uuid: rel_peer_id})
258
261
  WHERE related_rel_status = "deleted"
262
+ AND (rel_peer_db_id IS NULL OR %(id_func)s(rel_peer) = rel_peer_db_id)
259
263
  AND target_r_rel_1.from <= $at AND target_r_rel_1.to IS NULL
260
264
  AND target_r_rel_2.from <= $at AND target_r_rel_2.to IS NULL
261
265
  SET target_r_rel_1.to = $at
262
266
  SET target_r_rel_2.to = $at
263
267
  }
264
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, related_rel_status
268
+ WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status
265
269
  // ------------------------------
266
270
  // conditionally create new IS_RELATED relationships on target_branch, if necessary
267
271
  // ------------------------------
268
- CALL {
269
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, related_rel_status
272
+ CALL (n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, rel_name, rel_peer_id, rel_peer_db_id, related_rel_status) {
270
273
  MATCH (p:Node {uuid: rel_peer_id})
274
+ WHERE rel_peer_db_id IS NULL OR %(id_func)s(p) = rel_peer_db_id
271
275
  OPTIONAL MATCH (n)
272
276
  -[r_rel_1:IS_RELATED {branch: $target_branch, status: related_rel_status}]
273
277
  -(:Relationship {name: rel_name})
@@ -277,38 +281,34 @@ CALL {
277
281
  AND (r_rel_1.to >= $at OR r_rel_1.to IS NULL)
278
282
  AND r_rel_2.from <= $at
279
283
  AND (r_rel_2.to >= $at OR r_rel_2.to IS NULL)
280
- WITH n, r, r1_dir, r2_dir, r1_hierarchy, r2_hierarchy, p, related_rel_status, r_rel_1, r_rel_2
284
+ WITH p, r_rel_1, r_rel_2
281
285
  WHERE r_rel_1 IS NULL
282
286
  AND r_rel_2 IS NULL
283
287
  // ------------------------------
284
288
  // create IS_RELATED relationships with directions maintained from source
285
289
  // ------------------------------
286
- CALL {
287
- WITH n, r, r1_dir, r1_hierarchy, related_rel_status
290
+ CALL (n, r, r1_dir, r1_hierarchy, related_rel_status) {
288
291
  WITH n, r, r1_dir, r1_hierarchy, related_rel_status
289
292
  WHERE r1_dir = "r"
290
293
  CREATE (n)
291
294
  -[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status, hierarchy: r1_hierarchy}]
292
295
  ->(r)
293
296
  }
294
- CALL {
295
- WITH n, r, r1_dir, r1_hierarchy, related_rel_status
297
+ CALL (n, r, r1_dir, r1_hierarchy, related_rel_status) {
296
298
  WITH n, r, r1_dir, r1_hierarchy, related_rel_status
297
299
  WHERE r1_dir = "l"
298
300
  CREATE (n)
299
301
  <-[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status, hierarchy: r1_hierarchy}]
300
302
  -(r)
301
303
  }
302
- CALL {
303
- WITH r, p, r2_dir, r2_hierarchy, related_rel_status
304
+ CALL (r, p, r2_dir, r2_hierarchy, related_rel_status) {
304
305
  WITH r, p, r2_dir, r2_hierarchy, related_rel_status
305
306
  WHERE r2_dir = "r"
306
307
  CREATE (r)
307
308
  -[:IS_RELATED {branch: $target_branch, branch_level: $branch_level, from: $at, status: related_rel_status, hierarchy: r2_hierarchy}]
308
309
  ->(p)
309
310
  }
310
- CALL {
311
- WITH r, p, r2_dir, r2_hierarchy, related_rel_status
311
+ CALL (r, p, r2_dir, r2_hierarchy, related_rel_status) {
312
312
  WITH r, p, r2_dir, r2_hierarchy, related_rel_status
313
313
  WHERE r2_dir = "l"
314
314
  CREATE (r)
@@ -320,7 +320,7 @@ CALL {
320
320
  }
321
321
  }
322
322
  RETURN 1 AS done
323
- """
323
+ """ % {"id_func": db.get_id_function_name()}
324
324
  self.add_to_query(query=query)
325
325
 
326
326
 
@@ -334,6 +334,7 @@ class DiffMergePropertiesQuery(Query):
334
334
  property_diff_dicts: dict[str, Any],
335
335
  at: Timestamp,
336
336
  target_branch: Branch,
337
+ migrated_kinds_id_map: dict[str, str],
337
338
  **kwargs: Any,
338
339
  ) -> None:
339
340
  super().__init__(**kwargs)
@@ -341,6 +342,7 @@ class DiffMergePropertiesQuery(Query):
341
342
  self.at = at
342
343
  self.target_branch = target_branch
343
344
  self.source_branch_name = self.branch.name
345
+ self.migrated_kinds_id_map = migrated_kinds_id_map
344
346
 
345
347
  async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
346
348
  self.params = {
@@ -349,59 +351,70 @@ class DiffMergePropertiesQuery(Query):
349
351
  "branch_level": self.target_branch.hierarchy_level,
350
352
  "target_branch": self.target_branch.name,
351
353
  "source_branch": self.source_branch_name,
354
+ "migrated_kinds_id_map": self.migrated_kinds_id_map,
355
+ "migrated_kinds_uuids": list(self.migrated_kinds_id_map.keys()),
352
356
  }
353
357
  query = """
354
358
  UNWIND $property_diff_dicts AS attr_rel_prop_diff
355
- CALL {
359
+ WITH attr_rel_prop_diff, CASE
360
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
361
+ WHEN attr_rel_prop_diff.node_uuid IN $migrated_kinds_uuids THEN $migrated_kinds_id_map[attr_rel_prop_diff.node_uuid]
362
+ ELSE NULL
363
+ END AS node_db_id,
364
+ CASE
365
+ WHEN $migrated_kinds_uuids IS NULL THEN NULL
366
+ WHEN attr_rel_prop_diff.peer_uuid IN $migrated_kinds_uuids THEN $migrated_kinds_id_map[attr_rel_prop_diff.peer_uuid]
367
+ ELSE NULL
368
+ END AS peer_db_id
369
+ CALL (attr_rel_prop_diff, node_db_id, peer_db_id) {
356
370
  // ------------------------------
357
371
  // find the Attribute node
358
372
  // ------------------------------
359
- WITH attr_rel_prop_diff
360
- CALL {
361
- WITH attr_rel_prop_diff
373
+ CALL (attr_rel_prop_diff, node_db_id) {
362
374
  OPTIONAL MATCH (n:Node {uuid: attr_rel_prop_diff.node_uuid})
363
375
  -[has_attr:HAS_ATTRIBUTE]
364
376
  ->(attr:Attribute {name: attr_rel_prop_diff.attribute_name})
365
377
  WHERE attr_rel_prop_diff.attribute_name IS NOT NULL
378
+ AND (node_db_id IS NULL OR %(id_func)s(n) = node_db_id)
366
379
  AND has_attr.branch IN [$source_branch, $target_branch]
367
380
  RETURN attr
368
381
  ORDER BY has_attr.from DESC
369
382
  LIMIT 1
370
383
  }
371
- CALL {
372
- WITH attr_rel_prop_diff
384
+ CALL (attr_rel_prop_diff, node_db_id, peer_db_id) {
373
385
  OPTIONAL MATCH (n:Node {uuid: attr_rel_prop_diff.node_uuid})
374
386
  -[r1:IS_RELATED]
375
387
  -(rel:Relationship {name: attr_rel_prop_diff.relationship_id})
376
388
  -[r2:IS_RELATED]
377
- -(:Node {uuid: attr_rel_prop_diff.peer_uuid})
389
+ -(rel_peer:Node {uuid: attr_rel_prop_diff.peer_uuid})
378
390
  WHERE attr_rel_prop_diff.relationship_id IS NOT NULL
391
+ AND (node_db_id IS NULL OR %(id_func)s(n) = node_db_id)
392
+ AND (peer_db_id IS NULL OR %(id_func)s(rel_peer) = peer_db_id)
379
393
  AND r1.branch IN [$source_branch, $target_branch]
380
394
  AND r2.branch IN [$source_branch, $target_branch]
381
395
  RETURN rel
382
396
  ORDER BY r1.branch_level DESC, r2.branch_level DESC, r1.from DESC, r2.from DESC
383
397
  LIMIT 1
384
398
  }
385
- WITH attr_rel_prop_diff, COALESCE(attr, rel) AS attr_rel
399
+ WITH attr_rel_prop_diff, COALESCE(attr, rel) AS attr_rel, peer_db_id
400
+ WHERE attr_rel IS NOT NULL
386
401
  UNWIND attr_rel_prop_diff.properties AS property_diff
387
402
  // ------------------------------
388
403
  // handle updates for properties under this attribute/relationship
389
404
  // ------------------------------
390
- CALL {
391
- WITH attr_rel, property_diff
405
+ CALL (attr_rel, property_diff, peer_db_id) {
392
406
  // ------------------------------
393
407
  // identify the correct property node to link
394
408
  // ------------------------------
395
- CALL {
396
- WITH attr_rel, property_diff
409
+ CALL (attr_rel, property_diff, peer_db_id) {
397
410
  OPTIONAL MATCH (peer:Node {uuid: property_diff.value})
398
411
  WHERE property_diff.property_type IN ["HAS_SOURCE", "HAS_OWNER"]
412
+ AND (peer_db_id IS NULL OR %(id_func)s(peer) = peer_db_id)
399
413
  // ------------------------------
400
414
  // the serialized diff might not include the values for IS_VISIBLE and IS_PROTECTED in
401
415
  // some cases, so we need to figure them out here
402
416
  // ------------------------------
403
- CALL {
404
- WITH attr_rel, property_diff
417
+ CALL (attr_rel, property_diff) {
405
418
  OPTIONAL MATCH (attr_rel)-[r_vis_pro]->(bool:Boolean)
406
419
  WHERE property_diff.property_type IN ["IS_VISIBLE", "IS_PROTECTED"]
407
420
  AND r_vis_pro.branch IN [$source_branch, $target_branch]
@@ -411,12 +424,11 @@ CALL {
411
424
  ORDER BY r_vis_pro.from DESC
412
425
  LIMIT 1
413
426
  }
414
- CALL {
427
+ CALL (attr_rel, property_diff) {
415
428
  // ------------------------------
416
429
  // get the latest linked AttributeValue on the source b/c there could be multiple
417
430
  // with different is_default values
418
431
  // ------------------------------
419
- WITH attr_rel, property_diff
420
432
  OPTIONAL MATCH (attr_rel)-[r_attr_val:HAS_VALUE]->(av:AttributeValue)
421
433
  WHERE property_diff.property_type = "HAS_VALUE"
422
434
  AND (
@@ -430,7 +442,7 @@ CALL {
430
442
  }
431
443
  RETURN COALESCE (peer, bool, av) AS prop_node
432
444
  }
433
- WITH attr_rel, property_diff.property_type AS prop_type, prop_node, CASE
445
+ WITH attr_rel,property_diff.property_type AS prop_type, prop_node, CASE
434
446
  WHEN property_diff.action = "ADDED" THEN "active"
435
447
  WHEN property_diff.action = "REMOVED" THEN "deleted"
436
448
  ELSE NULL
@@ -438,8 +450,7 @@ CALL {
438
450
  // ------------------------------
439
451
  // set property edge.to, optionally, on target branch
440
452
  // ------------------------------
441
- CALL {
442
- WITH attr_rel, prop_rel_status, prop_type
453
+ CALL (attr_rel, prop_rel_status, prop_type) {
443
454
  OPTIONAL MATCH (attr_rel)
444
455
  -[target_r_prop {branch: $target_branch}]
445
456
  ->()
@@ -450,8 +461,7 @@ CALL {
450
461
  // ------------------------------
451
462
  // check for existing edge on target_branch
452
463
  // ------------------------------
453
- CALL {
454
- WITH attr_rel, prop_rel_status, prop_type, prop_node
464
+ CALL (attr_rel, prop_rel_status, prop_type, prop_node) {
455
465
  OPTIONAL MATCH (attr_rel)-[r_prop {branch: $target_branch}]->(prop_node)
456
466
  WHERE type(r_prop) = prop_type
457
467
  AND r_prop.status = prop_rel_status
@@ -459,48 +469,221 @@ CALL {
459
469
  AND (r_prop.to > $at OR r_prop.to IS NULL)
460
470
  RETURN r_prop
461
471
  }
462
- WITH attr_rel, prop_rel_status, prop_type, prop_node, r_prop
472
+ WITH attr_rel,prop_rel_status, prop_type, prop_node, r_prop
463
473
  WHERE r_prop IS NULL
464
474
  // ------------------------------
465
475
  // create new edge to prop_node on target_branch, if necessary
466
476
  // one subquery per possible edge type b/c edge type cannot be a variable
467
477
  // ------------------------------
468
- CALL {
469
- WITH attr_rel, prop_rel_status, prop_type, prop_node
478
+ CALL (attr_rel, prop_rel_status, prop_type, prop_node) {
470
479
  WITH attr_rel, prop_rel_status, prop_type, prop_node
471
480
  WHERE prop_type = "HAS_VALUE"
472
481
  CREATE (attr_rel)-[:HAS_VALUE { branch: $target_branch, branch_level: $branch_level, from: $at, status: prop_rel_status }]->(prop_node)
473
482
  }
474
- CALL {
475
- WITH attr_rel, prop_rel_status, prop_type, prop_node
483
+ CALL (attr_rel, prop_rel_status, prop_type, prop_node) {
476
484
  WITH attr_rel, prop_rel_status, prop_type, prop_node
477
485
  WHERE prop_type = "HAS_SOURCE"
478
486
  CREATE (attr_rel)-[:HAS_SOURCE { branch: $target_branch, branch_level: $branch_level, from: $at, status: prop_rel_status }]->(prop_node)
479
487
  }
480
- CALL {
481
- WITH attr_rel, prop_rel_status, prop_type, prop_node
488
+ CALL (attr_rel, prop_rel_status, prop_type, prop_node) {
482
489
  WITH attr_rel, prop_rel_status, prop_type, prop_node
483
490
  WHERE prop_type = "HAS_OWNER"
484
491
  CREATE (attr_rel)-[:HAS_OWNER { branch: $target_branch, branch_level: $branch_level, from: $at, status: prop_rel_status }]->(prop_node)
485
492
  }
486
- CALL {
487
- WITH attr_rel, prop_rel_status, prop_type, prop_node
493
+ CALL (attr_rel, prop_rel_status, prop_type, prop_node) {
488
494
  WITH attr_rel, prop_rel_status, prop_type, prop_node
489
495
  WHERE prop_type = "IS_VISIBLE"
490
496
  CREATE (attr_rel)-[:IS_VISIBLE { branch: $target_branch, branch_level: $branch_level, from: $at, status: prop_rel_status }]->(prop_node)
491
497
  }
492
- CALL {
493
- WITH attr_rel, prop_rel_status, prop_type, prop_node
498
+ CALL (attr_rel, prop_rel_status, prop_type, prop_node) {
494
499
  WITH attr_rel, prop_rel_status, prop_type, prop_node
495
500
  WHERE prop_type = "IS_PROTECTED"
496
501
  CREATE (attr_rel)-[:IS_PROTECTED { branch: $target_branch, branch_level: $branch_level, from: $at, status: prop_rel_status }]->(prop_node)
497
502
  }
498
503
  }
499
504
  }
500
- """
505
+ """ % {"id_func": db.get_id_function_name()}
501
506
  self.add_to_query(query=query)
502
507
 
503
508
 
509
+ class DiffMergeMigratedKindsQuery(Query):
510
+ name = "diff_merge_migrated_kinds"
511
+ type = QueryType.WRITE
512
+ insert_return = False
513
+
514
+ def __init__(
515
+ self,
516
+ migrated_uuids: list[str],
517
+ at: Timestamp,
518
+ target_branch: Branch,
519
+ **kwargs: Any,
520
+ ) -> None:
521
+ super().__init__(**kwargs)
522
+ self.migrated_uuids = migrated_uuids
523
+ self.at = at
524
+ self.target_branch = target_branch
525
+ self.source_branch_name = self.branch.name
526
+
527
+ async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
528
+ self.params = {
529
+ "migrated_uuids": self.migrated_uuids,
530
+ "at": self.at.to_string(),
531
+ "branch_level": self.target_branch.hierarchy_level,
532
+ "target_branch": self.target_branch.name,
533
+ "source_branch": self.source_branch_name,
534
+ }
535
+ query = """
536
+ MATCH (n:Node)
537
+ WHERE n.uuid IN $migrated_uuids
538
+ CALL (n) {
539
+ // --------------
540
+ // for each migrated node (created or deleted), find its latest edges on the source branch,
541
+ // check if they exist on the target, create them if not
542
+ // --------------
543
+ MATCH (n)-[]-(peer)
544
+ WITH DISTINCT n, peer
545
+ CALL (n, peer) {
546
+ // --------------
547
+ // get the latest outbound edge for each type between n and peer
548
+ // --------------
549
+ MATCH (n)-[e {branch: $source_branch}]->(peer)
550
+ WHERE e.from <= $at AND e.to IS NULL
551
+ WITH e, type(e) AS edge_type
552
+ ORDER BY edge_type, e.from DESC
553
+ WITH edge_type, head(collect(e)) AS latest_source_edge
554
+ RETURN edge_type, latest_source_edge
555
+ }
556
+ CALL (n, peer, edge_type) {
557
+ // --------------
558
+ // for each n, peer, edge_type, get the latest edge on target
559
+ // --------------
560
+ OPTIONAL MATCH (n)-[e {branch: $target_branch}]->(peer)
561
+ WHERE type(e) = edge_type AND e.from <= $at
562
+ RETURN e AS latest_target_edge
563
+ ORDER BY e.from DESC
564
+ LIMIT 1
565
+ }
566
+ // --------------
567
+ // ignore edges of this type that already have the correct status on the target branch
568
+ // --------------
569
+ WITH n, peer, edge_type, latest_source_edge, latest_target_edge
570
+ WHERE (latest_target_edge IS NULL AND latest_source_edge.status = "active")
571
+ OR latest_source_edge.status <> latest_target_edge.status
572
+ CALL (latest_source_edge, latest_target_edge) {
573
+ // --------------
574
+ // set the to time on active target branch edges that we are setting to deleted
575
+ // --------------
576
+ WITH latest_target_edge WHERE latest_target_edge IS NOT NULL
577
+ AND latest_source_edge.status = "deleted"
578
+ AND latest_target_edge.status = "active"
579
+ AND latest_target_edge.to IS NULL
580
+ SET latest_target_edge.to = $at
581
+ }
582
+ // --------------
583
+ // create the outbound edges on the target branch, one subquery per possible type
584
+ // --------------
585
+ CALL (n, latest_source_edge, peer, edge_type) {
586
+ WITH edge_type WHERE edge_type = "IS_PART_OF"
587
+ CREATE (n)-[new_edge:IS_PART_OF]->(peer)
588
+ SET new_edge = properties(latest_source_edge)
589
+ SET new_edge.from = $at
590
+ SET new_edge.branch_level = $branch_level
591
+ SET new_edge.branch = $target_branch
592
+ }
593
+ CALL (n, latest_source_edge, peer, edge_type) {
594
+ WITH edge_type
595
+ WHERE edge_type = "IS_RELATED"
596
+ CREATE (n)-[new_edge:IS_RELATED]->(peer)
597
+ SET new_edge = properties(latest_source_edge)
598
+ SET new_edge.from = $at
599
+ SET new_edge.branch_level = $branch_level
600
+ SET new_edge.branch = $target_branch
601
+ }
602
+ CALL (n, latest_source_edge, peer, edge_type) {
603
+ WITH edge_type
604
+ WHERE edge_type = "HAS_ATTRIBUTE"
605
+ CREATE (n)-[new_edge:HAS_ATTRIBUTE]->(peer)
606
+ SET new_edge = properties(latest_source_edge)
607
+ SET new_edge.from = $at
608
+ SET new_edge.branch_level = $branch_level
609
+ SET new_edge.branch = $target_branch
610
+ }
611
+ // --------------
612
+ // do all of this again for inbound edges
613
+ // --------------
614
+ WITH DISTINCT n, peer
615
+ CALL (n, peer) {
616
+ // --------------
617
+ // get the latest inbound edge for each type between n and peer
618
+ // --------------
619
+ MATCH (n)<-[e {branch: $source_branch}]-(peer)
620
+ WHERE e.from <= $at AND e.to IS NULL
621
+ WITH e, type(e) AS edge_type
622
+ ORDER BY edge_type, e.from DESC
623
+ WITH edge_type, head(collect(e)) AS latest_source_edge
624
+ RETURN edge_type, latest_source_edge
625
+ }
626
+ CALL (n, peer, edge_type) {
627
+ // --------------
628
+ // for each n, peer, edge_type, get the latest edge on target
629
+ // --------------
630
+ OPTIONAL MATCH (n)<-[e {branch: $target_branch}]-(peer)
631
+ WHERE type(e) = edge_type AND e.from <= $at
632
+ RETURN e AS latest_target_edge
633
+ ORDER BY e.from DESC
634
+ LIMIT 1
635
+ }
636
+ // --------------
637
+ // ignore edges of this type that already have the correct status on the target branch
638
+ // --------------
639
+ WITH n, peer, edge_type, latest_source_edge, latest_target_edge
640
+ WHERE latest_target_edge IS NULL OR latest_source_edge.status <> latest_target_edge.status
641
+ CALL (latest_source_edge, latest_target_edge) {
642
+ // --------------
643
+ // set the to time on active target branch edges that we are setting to deleted
644
+ // --------------
645
+ WITH latest_target_edge
646
+ WHERE latest_target_edge IS NOT NULL
647
+ AND latest_source_edge.status = "deleted"
648
+ AND latest_target_edge.status = "active"
649
+ AND latest_target_edge.to IS NULL
650
+ SET latest_target_edge.to = $at
651
+ }
652
+ // --------------
653
+ // create the outbound edges on the target branch, one subquery per possible type
654
+ // --------------
655
+ CALL (n, latest_source_edge, peer, edge_type) {
656
+ WITH edge_type
657
+ WHERE edge_type = "IS_RELATED"
658
+ CREATE (n)<-[new_edge:IS_RELATED]-(peer)
659
+ SET new_edge = properties(latest_source_edge)
660
+ SET new_edge.from = $at
661
+ SET new_edge.branch_level = $branch_level
662
+ SET new_edge.branch = $target_branch
663
+ }
664
+ CALL (n, latest_source_edge, peer, edge_type) {
665
+ WITH edge_type
666
+ WHERE edge_type = "HAS_OWNER"
667
+ CREATE (n)<-[new_edge:HAS_OWNER]-(peer)
668
+ SET new_edge = properties(latest_source_edge)
669
+ SET new_edge.from = $at
670
+ SET new_edge.branch_level = $branch_level
671
+ SET new_edge.branch = $target_branch
672
+ }
673
+ CALL (n, latest_source_edge, peer, edge_type) {
674
+ WITH edge_type
675
+ WHERE edge_type = "HAS_SOURCE"
676
+ CREATE (n)<-[new_edge:HAS_SOURCE]-(peer)
677
+ SET new_edge = properties(latest_source_edge)
678
+ SET new_edge.from = $at
679
+ SET new_edge.branch_level = $branch_level
680
+ SET new_edge.branch = $target_branch
681
+ }
682
+ }
683
+ """
684
+ self.add_to_query(query)
685
+
686
+
504
687
  class DiffMergeRollbackQuery(Query):
505
688
  name = "diff_merge_rollback"
506
689
  type = QueryType.WRITE