infrahub-server 1.5.0b1__py3-none-any.whl → 1.5.0b2__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 (118) hide show
  1. infrahub/api/internal.py +2 -0
  2. infrahub/api/oauth2.py +13 -19
  3. infrahub/api/oidc.py +15 -21
  4. infrahub/api/schema.py +24 -3
  5. infrahub/artifacts/models.py +2 -1
  6. infrahub/auth.py +137 -3
  7. infrahub/cli/__init__.py +2 -0
  8. infrahub/cli/db.py +83 -102
  9. infrahub/cli/dev.py +118 -0
  10. infrahub/cli/tasks.py +46 -0
  11. infrahub/cli/upgrade.py +30 -3
  12. infrahub/computed_attribute/tasks.py +20 -8
  13. infrahub/core/attribute.py +10 -2
  14. infrahub/core/branch/enums.py +1 -1
  15. infrahub/core/branch/models.py +7 -3
  16. infrahub/core/branch/tasks.py +68 -7
  17. infrahub/core/constants/__init__.py +3 -0
  18. infrahub/core/diff/query/artifact.py +1 -0
  19. infrahub/core/diff/query/field_summary.py +1 -0
  20. infrahub/core/graph/__init__.py +1 -1
  21. infrahub/core/initialization.py +5 -2
  22. infrahub/core/migrations/__init__.py +3 -0
  23. infrahub/core/migrations/exceptions.py +4 -0
  24. infrahub/core/migrations/graph/__init__.py +10 -13
  25. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  26. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  27. infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
  28. infrahub/core/migrations/graph/m041_profile_attrs_in_db.py +145 -0
  29. infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py +164 -0
  30. infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py +866 -0
  31. infrahub/core/migrations/query/__init__.py +7 -8
  32. infrahub/core/migrations/query/attribute_add.py +8 -6
  33. infrahub/core/migrations/query/attribute_remove.py +134 -0
  34. infrahub/core/migrations/runner.py +54 -0
  35. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  36. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  37. infrahub/core/migrations/schema/node_attribute_add.py +30 -2
  38. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  39. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  40. infrahub/core/migrations/schema/node_remove.py +2 -1
  41. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  42. infrahub/core/migrations/shared.py +48 -14
  43. infrahub/core/node/__init__.py +16 -11
  44. infrahub/core/node/create.py +46 -63
  45. infrahub/core/node/lock_utils.py +70 -44
  46. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  47. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  48. infrahub/core/node/resource_manager/number_pool.py +2 -1
  49. infrahub/core/query/attribute.py +55 -0
  50. infrahub/core/query/ipam.py +1 -0
  51. infrahub/core/query/node.py +9 -3
  52. infrahub/core/query/relationship.py +1 -0
  53. infrahub/core/schema/__init__.py +56 -0
  54. infrahub/core/schema/attribute_schema.py +4 -0
  55. infrahub/core/schema/definitions/internal.py +2 -2
  56. infrahub/core/schema/generated/attribute_schema.py +2 -2
  57. infrahub/core/schema/manager.py +22 -1
  58. infrahub/core/schema/schema_branch.py +180 -22
  59. infrahub/database/graph.py +21 -0
  60. infrahub/display_labels/tasks.py +13 -7
  61. infrahub/events/branch_action.py +27 -1
  62. infrahub/generators/tasks.py +3 -7
  63. infrahub/git/base.py +4 -1
  64. infrahub/git/integrator.py +1 -1
  65. infrahub/git/models.py +2 -1
  66. infrahub/git/repository.py +22 -5
  67. infrahub/git/tasks.py +66 -10
  68. infrahub/git/utils.py +123 -1
  69. infrahub/graphql/api/endpoints.py +14 -4
  70. infrahub/graphql/manager.py +4 -9
  71. infrahub/graphql/mutations/convert_object_type.py +11 -1
  72. infrahub/graphql/mutations/display_label.py +17 -10
  73. infrahub/graphql/mutations/hfid.py +17 -10
  74. infrahub/graphql/mutations/ipam.py +54 -35
  75. infrahub/graphql/mutations/main.py +27 -28
  76. infrahub/graphql/schema_sort.py +170 -0
  77. infrahub/graphql/types/branch.py +4 -1
  78. infrahub/graphql/types/enums.py +3 -0
  79. infrahub/hfid/tasks.py +13 -7
  80. infrahub/lock.py +52 -12
  81. infrahub/message_bus/types.py +2 -1
  82. infrahub/permissions/constants.py +2 -0
  83. infrahub/proposed_change/tasks.py +25 -16
  84. infrahub/server.py +6 -2
  85. infrahub/services/__init__.py +2 -2
  86. infrahub/services/adapters/http/__init__.py +5 -0
  87. infrahub/services/adapters/workflow/worker.py +14 -3
  88. infrahub/task_manager/event.py +5 -0
  89. infrahub/task_manager/models.py +7 -0
  90. infrahub/task_manager/task.py +73 -0
  91. infrahub/trigger/setup.py +13 -4
  92. infrahub/trigger/tasks.py +3 -0
  93. infrahub/workers/dependencies.py +10 -1
  94. infrahub/workers/infrahub_async.py +10 -2
  95. infrahub/workflows/catalogue.py +8 -0
  96. infrahub/workflows/initialization.py +5 -0
  97. infrahub/workflows/utils.py +2 -1
  98. infrahub_sdk/client.py +13 -10
  99. infrahub_sdk/config.py +29 -2
  100. infrahub_sdk/ctl/schema.py +22 -7
  101. infrahub_sdk/schema/__init__.py +32 -4
  102. infrahub_sdk/spec/models.py +7 -0
  103. infrahub_sdk/spec/object.py +37 -102
  104. infrahub_sdk/spec/processors/__init__.py +0 -0
  105. infrahub_sdk/spec/processors/data_processor.py +10 -0
  106. infrahub_sdk/spec/processors/factory.py +34 -0
  107. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  108. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/METADATA +3 -1
  109. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/RECORD +115 -101
  110. infrahub_testcontainers/container.py +114 -2
  111. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  112. infrahub_testcontainers/docker-compose.test.yml +5 -0
  113. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
  114. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +0 -97
  115. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +0 -86
  116. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/LICENSE.txt +0 -0
  117. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/WHEEL +0 -0
  118. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,866 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from itertools import chain
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import ujson
8
+ from rich.progress import Progress, TaskID
9
+
10
+ from infrahub.core import registry
11
+ from infrahub.core.branch import Branch
12
+ from infrahub.core.constants import GLOBAL_BRANCH_NAME, BranchSupportType, RelationshipDirection
13
+ from infrahub.core.initialization import get_root_node
14
+ from infrahub.core.migrations.shared import MigrationResult
15
+ from infrahub.core.query import Query, QueryType
16
+ from infrahub.types import is_large_attribute_type
17
+
18
+ from ..shared import MigrationRequiringRebase
19
+ from .load_schema_branch import get_or_load_schema_branch
20
+
21
+ if TYPE_CHECKING:
22
+ from infrahub.core.schema import AttributeSchema, NodeSchema
23
+ from infrahub.core.schema.basenode_schema import SchemaAttributePath
24
+ from infrahub.core.schema.schema_branch import SchemaBranch
25
+ from infrahub.database import InfrahubDatabase
26
+
27
+
28
+ class DefaultBranchNodeCount(Query):
29
+ """
30
+ Get the number of Node vertices on the given branches that are not in the kinds_to_skip list
31
+ Only works for default and global branches. Non-default branches would only return a count of nodes
32
+ created on the given branches
33
+ """
34
+
35
+ name = "get_branch_node_count"
36
+ type = QueryType.READ
37
+
38
+ def __init__(self, kinds_to_skip: list[str], **kwargs: Any) -> None:
39
+ super().__init__(**kwargs)
40
+ self.kinds_to_skip = kinds_to_skip
41
+
42
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
43
+ self.params = {
44
+ "branch_names": [registry.default_branch, GLOBAL_BRANCH_NAME],
45
+ "kinds_to_skip": self.kinds_to_skip,
46
+ }
47
+ query = """
48
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
49
+ WHERE NOT n.kind IN $kinds_to_skip
50
+ AND e.branch IN $branch_names
51
+ AND e.status = "active"
52
+ AND e.to IS NULL
53
+ AND NOT exists((n)-[:IS_PART_OF {branch: e.branch, status: "deleted"}]->(:Root))
54
+ WITH count(*) AS num_nodes
55
+ """
56
+ self.add_to_query(query)
57
+ self.return_labels = ["num_nodes"]
58
+
59
+ def get_num_nodes(self) -> int:
60
+ result = self.get_result()
61
+ if not result:
62
+ return 0
63
+ return result.get_as_type(label="num_nodes", return_type=int)
64
+
65
+
66
+ class GetResultMapQuery(Query):
67
+ def get_result_map(self, schema_paths: list[SchemaAttributePath]) -> dict[str, list[str | None]]:
68
+ """
69
+ Get the values for the given schema paths for all the Nodes captured by this query
70
+ """
71
+ # the query results for attribute and schema paths are unordered
72
+ # so we make this list of keys for ordering the results from the query
73
+ schema_path_keys: list[tuple[str, RelationshipDirection, str] | str] = []
74
+ for schema_path in schema_paths:
75
+ if schema_path.is_type_attribute and schema_path.attribute_schema:
76
+ path_key: str | tuple[str, RelationshipDirection, str] = schema_path.attribute_schema.name
77
+ elif schema_path.is_type_relationship and schema_path.relationship_schema and schema_path.attribute_schema:
78
+ path_key = (
79
+ schema_path.relationship_schema.get_identifier(),
80
+ schema_path.relationship_schema.direction,
81
+ schema_path.attribute_schema.name,
82
+ )
83
+ schema_path_keys.append(path_key)
84
+
85
+ result_map: dict[str, list[str | None]] = {}
86
+ for result in self.get_results():
87
+ node_uuid = result.get_as_type(label="n_uuid", return_type=str)
88
+
89
+ # for each node, build a map of the schema path key to value so that they
90
+ # can be ordered correctly for the input `schema_paths`
91
+ schema_path_value_map: dict[str | tuple[str, RelationshipDirection, str], Any] = {}
92
+ attr_values_tuples: list[tuple[str, Any]] = result.get_as_type(label="attr_vals_list", return_type=list)
93
+ for attr_value_tuple in attr_values_tuples:
94
+ attr_name = attr_value_tuple[0]
95
+ attr_value = attr_value_tuple[1]
96
+ schema_path_value_map[attr_name] = attr_value
97
+
98
+ relationship_values_tuples: list[tuple[str, str, str, Any]] = result.get_as_type(
99
+ label="peer_attr_vals_list", return_type=list
100
+ )
101
+ for rel_value_tuple in relationship_values_tuples:
102
+ rel_name = rel_value_tuple[0]
103
+ direction_raw = rel_value_tuple[1]
104
+ direction = RelationshipDirection.BIDIR
105
+ match direction_raw:
106
+ case "outbound":
107
+ direction = RelationshipDirection.OUTBOUND
108
+ case "inbound":
109
+ direction = RelationshipDirection.INBOUND
110
+ peer_attr_name = rel_value_tuple[2]
111
+ peer_val = rel_value_tuple[3]
112
+ schema_path_value_map[rel_name, direction, peer_attr_name] = peer_val
113
+
114
+ schema_path_values: list[str | None] = []
115
+ for schema_path_key in schema_path_keys:
116
+ value = schema_path_value_map.get(schema_path_key)
117
+ schema_path_values.append(str(value) if value is not None else None)
118
+ result_map[node_uuid] = schema_path_values
119
+ return result_map
120
+
121
+
122
+ class GetPathDetailsBranchQuery(GetResultMapQuery):
123
+ name = "get_path_details_branch"
124
+ type = QueryType.READ
125
+ insert_limit = False
126
+
127
+ def __init__(
128
+ self, schema_kind: str, schema_paths: list[SchemaAttributePath], updates_only: bool = True, **kwargs: Any
129
+ ) -> None:
130
+ super().__init__(**kwargs)
131
+
132
+ if self.branch.name in [registry.default_branch, GLOBAL_BRANCH_NAME]:
133
+ raise ValueError("This query can only be used on non-default branches")
134
+ self.schema_kind = schema_kind
135
+ self.updates_only = updates_only
136
+ self.attribute_names = []
137
+ self.bidir_rel_attr_map: dict[str, list[str]] = defaultdict(list)
138
+ self.outbound_rel_attr_map: dict[str, list[str]] = defaultdict(list)
139
+ self.inbound_rel_attr_map: dict[str, list[str]] = defaultdict(list)
140
+ for schema_path in schema_paths:
141
+ if schema_path.is_type_attribute and schema_path.attribute_schema:
142
+ self.attribute_names.append(schema_path.attribute_schema.name)
143
+ elif schema_path.is_type_relationship and schema_path.relationship_schema and schema_path.attribute_schema:
144
+ key = schema_path.relationship_schema.get_identifier()
145
+ value = schema_path.attribute_schema.name
146
+ if schema_path.relationship_schema.direction is RelationshipDirection.BIDIR:
147
+ self.bidir_rel_attr_map[key].append(value)
148
+ elif schema_path.relationship_schema.direction is RelationshipDirection.OUTBOUND:
149
+ self.outbound_rel_attr_map[key].append(value)
150
+ elif schema_path.relationship_schema.direction is RelationshipDirection.INBOUND:
151
+ self.inbound_rel_attr_map[key].append(value)
152
+
153
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
154
+ branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
155
+ self.params.update(branch_filter_params)
156
+ self.params.update(
157
+ {
158
+ "branch_name": self.branch.name,
159
+ "attribute_names": self.attribute_names,
160
+ "outbound_rel_ids": list(self.outbound_rel_attr_map.keys()),
161
+ "inbound_rel_ids": list(self.inbound_rel_attr_map.keys()),
162
+ "bidirectional_rel_ids": list(self.bidir_rel_attr_map.keys()),
163
+ "outbound_rel_attr_map": self.outbound_rel_attr_map,
164
+ "inbound_rel_attr_map": self.inbound_rel_attr_map,
165
+ "bidirectional_rel_attr_map": self.bidir_rel_attr_map,
166
+ "offset": self.offset,
167
+ "limit": self.limit,
168
+ }
169
+ )
170
+ get_active_nodes_query = """
171
+ // ------------
172
+ // Get the active nodes of the given kind on the branches
173
+ // ------------
174
+ MATCH (n:%(schema_kind)s)-[r:IS_PART_OF]->(:Root)
175
+ WHERE %(branch_filter)s
176
+ WITH DISTINCT n
177
+ CALL (n) {
178
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
179
+ WHERE %(branch_filter)s
180
+ RETURN r.status = "active" AS is_active
181
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
182
+ LIMIT 1
183
+ }
184
+ WITH n, is_active
185
+ WHERE is_active = TRUE
186
+ """ % {"schema_kind": self.schema_kind, "branch_filter": branch_filter}
187
+ self.add_to_query(get_active_nodes_query)
188
+
189
+ if self.updates_only:
190
+ updated_nodes_filter_query = """
191
+ // ------------
192
+ // filter to any nodes that might have changes on the branch we care about
193
+ // ------------
194
+ OPTIONAL MATCH (n)-[r1:HAS_ATTRIBUTE]->(attr:Attribute)-[r2:HAS_VALUE]->(attr_val:AttributeValue)
195
+ WHERE attr.name in $attribute_names
196
+ AND r2.branch = $branch_name
197
+ AND r2.status = "active"
198
+ AND r2.to IS NULL
199
+ WITH n, attr_val IS NOT NULL AS has_attr_update
200
+ OPTIONAL MATCH (n)-[r1:IS_RELATED]-(rel:Relationship)-[r2:IS_RELATED]-(peer:Node)-[r3:HAS_ATTRIBUTE]-(attr:Attribute)-[r4:HAS_VALUE]->(attr_val)
201
+ WHERE rel.name IN $bidirectional_rel_ids + $outbound_rel_ids + $inbound_rel_ids
202
+ AND (
203
+ attr.name IN $outbound_rel_attr_map[rel.name]
204
+ OR attr.name IN $inbound_rel_attr_map[rel.name]
205
+ OR attr.name IN $bidirectional_rel_attr_map[rel.name]
206
+ )
207
+ AND $branch_name IN [r1.branch, r2.branch, r3.branch, r4.branch]
208
+ WITH n, has_attr_update, attr_val IS NOT NULL AS has_rel_update
209
+ WITH n, any(x IN collect(has_attr_update OR has_rel_update) WHERE x = TRUE) AS has_update
210
+ WITH n, has_update
211
+ WHERE has_update = TRUE
212
+ """
213
+ self.add_to_query(updated_nodes_filter_query)
214
+
215
+ get_node_details_query = """
216
+ // ------------
217
+ // Order and limit the Nodes
218
+ // ------------
219
+ ORDER BY elementId(n)
220
+ SKIP toInteger($offset)
221
+ LIMIT toInteger($limit)
222
+ // ------------
223
+ // for every possibly updated node
224
+ // get all the attribute values on this branch
225
+ // ------------
226
+ OPTIONAL MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute)
227
+ WHERE attr.name IN $attribute_names
228
+ WITH DISTINCT n, attr
229
+ CALL (n, attr) {
230
+ OPTIONAL MATCH (n)-[r1:HAS_ATTRIBUTE]->(attr)-[r2:HAS_VALUE]->(attr_val)
231
+ WHERE all(r in [r1, r2] WHERE %(branch_filter)s)
232
+ RETURN attr_val.value AS attr_value, r1.status = "active" AND r2.status = "active" AS is_active
233
+ ORDER BY r2.branch_level DESC, r2.from DESC, r2.status ASC, r1.branch_level DESC, r1.from DESC, r1.status ASC
234
+ LIMIT 1
235
+ }
236
+ WITH n, attr, attr_value
237
+ WHERE is_active = TRUE
238
+ WITH n, collect([attr.name, attr_value]) AS attr_vals_list
239
+ // ------------
240
+ // for every possibly updated node
241
+ // get all the relationships on this branch
242
+ // ------------
243
+ OPTIONAL MATCH (n)-[:IS_RELATED]-(rel:Relationship)
244
+ WHERE rel.name IN $bidirectional_rel_ids + $outbound_rel_ids + $inbound_rel_ids
245
+ WITH DISTINCT n, attr_vals_list, rel
246
+ CALL (n, rel) {
247
+ OPTIONAL MATCH (n)-[r1:IS_RELATED]-(rel)-[r2:IS_RELATED]-(peer:Node)
248
+ WHERE all(r in [r1, r2] WHERE %(branch_filter)s)
249
+ AND (
250
+ (startNode(r1) = n AND startNode(r2) = rel AND rel.name IN $outbound_rel_ids)
251
+ OR (startNode(r1) = rel AND startNode(r2) = peer AND rel.name IN $inbound_rel_ids)
252
+ OR (startNode(r1) = n AND startNode(r2) = peer AND rel.name IN $bidirectional_rel_ids)
253
+ )
254
+ RETURN
255
+ peer,
256
+ r1.status = "active" AND r2.status = "active" AS is_active,
257
+ CASE
258
+ WHEN startNode(r1) = n AND startNode(r2) = rel AND rel.name IN $outbound_rel_ids THEN "outbound"
259
+ WHEN startNode(r1) = rel AND startNode(r2) = peer AND rel.name IN $inbound_rel_ids THEN "inbound"
260
+ ELSE "bidir"
261
+ END AS direction
262
+ ORDER BY r2.branch_level DESC, r2.from DESC, r2.status ASC, r1.branch_level DESC, r1.from DESC, r1.status ASC
263
+ LIMIT 1
264
+ }
265
+ // ------------
266
+ // get the attribute values that we care about for each relationship
267
+ // ------------
268
+ WITH n, attr_vals_list, rel.name AS rel_name, direction, peer
269
+ WHERE is_active = TRUE OR rel_name IS NULL
270
+ WITH *, CASE
271
+ WHEN direction = "outbound" THEN $outbound_rel_attr_map[rel_name]
272
+ WHEN direction = "inbound" THEN $inbound_rel_attr_map[rel_name]
273
+ ELSE $bidirectional_rel_attr_map[rel_name]
274
+ END AS peer_attr_names
275
+ UNWIND COALESCE(peer_attr_names, [NULL]) AS peer_attr_name
276
+ CALL (rel_name, direction, peer, peer_attr_name){
277
+ OPTIONAL MATCH (peer)-[r1:HAS_ATTRIBUTE]->(attr:Attribute)-[r2:HAS_VALUE]->(attr_val)
278
+ WHERE attr.name = peer_attr_name
279
+ AND all(r in [r1, r2] WHERE %(branch_filter)s)
280
+ RETURN attr_val.value AS peer_attr_value, r1.status = "active" AND r2.status = "active" AS is_active
281
+ ORDER BY r2.branch_level DESC, r2.from DESC, r2.status ASC, r1.branch_level DESC, r1.from DESC, r1.status ASC
282
+ LIMIT 1
283
+ }
284
+ // ------------
285
+ // collect everything to return a pair of lists with each node UUID
286
+ // ------------
287
+ WITH DISTINCT n, attr_vals_list, rel_name, peer, direction, peer_attr_name, peer_attr_value
288
+ WITH n, attr_vals_list, collect([rel_name, direction, peer_attr_name, peer_attr_value]) AS peer_attr_vals_list
289
+ """ % {"branch_filter": branch_filter}
290
+ self.add_to_query(get_node_details_query)
291
+ self.return_labels = ["n.uuid AS n_uuid", "attr_vals_list", "peer_attr_vals_list"]
292
+
293
+
294
+ class GetPathDetailsDefaultBranch(GetResultMapQuery):
295
+ """
296
+ Get the values of the given schema paths for the given kind of node on the default and global branches
297
+ Supports limit and offset for pagination
298
+ """
299
+
300
+ name = "get_path_details_default_branch"
301
+ type = QueryType.READ
302
+ insert_limit = False
303
+
304
+ def __init__(self, schema_kind: str, schema_paths: list[SchemaAttributePath], **kwargs: Any) -> None:
305
+ super().__init__(**kwargs)
306
+
307
+ self.branch_names = [registry.default_branch, GLOBAL_BRANCH_NAME]
308
+ self.schema_kind = schema_kind
309
+ self.attribute_names = []
310
+ self.bidir_rel_attr_map: dict[str, list[str]] = defaultdict(list)
311
+ self.outbound_rel_attr_map: dict[str, list[str]] = defaultdict(list)
312
+ self.inbound_rel_attr_map: dict[str, list[str]] = defaultdict(list)
313
+ for schema_path in schema_paths:
314
+ if schema_path.is_type_attribute and schema_path.attribute_schema:
315
+ self.attribute_names.append(schema_path.attribute_schema.name)
316
+ elif schema_path.is_type_relationship and schema_path.relationship_schema and schema_path.attribute_schema:
317
+ key = schema_path.relationship_schema.get_identifier()
318
+ value = schema_path.attribute_schema.name
319
+ if schema_path.relationship_schema.direction is RelationshipDirection.BIDIR:
320
+ self.bidir_rel_attr_map[key].append(value)
321
+ elif schema_path.relationship_schema.direction is RelationshipDirection.OUTBOUND:
322
+ self.outbound_rel_attr_map[key].append(value)
323
+ elif schema_path.relationship_schema.direction is RelationshipDirection.INBOUND:
324
+ self.inbound_rel_attr_map[key].append(value)
325
+
326
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
327
+ self.params = {
328
+ "branch_names": self.branch_names,
329
+ "attribute_names": self.attribute_names,
330
+ "outbound_rel_ids": list(self.outbound_rel_attr_map.keys()),
331
+ "inbound_rel_ids": list(self.inbound_rel_attr_map.keys()),
332
+ "bidirectional_rel_ids": list(self.bidir_rel_attr_map.keys()),
333
+ "outbound_rel_attr_map": self.outbound_rel_attr_map,
334
+ "inbound_rel_attr_map": self.inbound_rel_attr_map,
335
+ "bidirectional_rel_attr_map": self.bidir_rel_attr_map,
336
+ "offset": self.offset,
337
+ "limit": self.limit,
338
+ }
339
+ get_details_query = """
340
+ // ------------
341
+ // Get the active nodes of the given kind on the branches
342
+ // ------------
343
+ MATCH (n:%(schema_kind)s)-[e:IS_PART_OF]->(:Root)
344
+ WHERE e.branch IN $branch_names
345
+ AND e.to IS NULL
346
+ AND e.status = "active"
347
+ // ------------
348
+ // Order and limit the Nodes
349
+ // ------------
350
+ WITH DISTINCT n
351
+ ORDER BY elementId(n)
352
+ SKIP toInteger($offset)
353
+ LIMIT toInteger($limit)
354
+ // ------------
355
+ // Get the values for the attribute schema paths of the Nodes, if any
356
+ // ------------
357
+ OPTIONAL MATCH (n)-[e:HAS_ATTRIBUTE]->(attr:Attribute)
358
+ WHERE attr.name IN $attribute_names
359
+ AND e.branch IN $branch_names
360
+ AND e.to IS NULL
361
+ AND e.status = "active"
362
+ WITH n, attr
363
+ OPTIONAL MATCH (attr)-[e:HAS_VALUE]->(attr_val:AttributeValue)
364
+ WHERE e.branch IN $branch_names
365
+ AND e.to IS NULL
366
+ AND e.status = "active"
367
+ WITH n, collect([attr.name, attr_val.value]) AS attr_vals_list
368
+ // ------------
369
+ // Get the values for the relationship schema paths of the Nodes, if any
370
+ // ------------
371
+ OPTIONAL MATCH (n)-[e1:IS_RELATED]-(rel:Relationship)-[e2:IS_RELATED]-(peer:Node)
372
+ WHERE rel.name IN $bidirectional_rel_ids + $outbound_rel_ids + $inbound_rel_ids
373
+ AND e1.branch IN $branch_names
374
+ AND e1.to IS NULL
375
+ AND e1.status = "active"
376
+ AND e2.branch IN $branch_names
377
+ AND e2.to IS NULL
378
+ AND e2.status = "active"
379
+ AND (
380
+ (startNode(e1) = n AND startNode(e2) = rel AND rel.name IN $outbound_rel_ids)
381
+ OR (startNode(e1) = rel AND startNode(e2) = peer AND rel.name IN $inbound_rel_ids)
382
+ OR (startNode(e1) = n AND startNode(e2) = peer AND rel.name IN $bidirectional_rel_ids)
383
+ )
384
+ WITH DISTINCT n, attr_vals_list, rel.name AS rel_name, peer, CASE
385
+ WHEN startNode(e1) = n AND startNode(e2) = rel AND rel.name IN $outbound_rel_ids THEN "outbound"
386
+ WHEN startNode(e1) = rel AND startNode(e2) = peer AND rel.name IN $inbound_rel_ids THEN "inbound"
387
+ ELSE "bidir"
388
+ END AS direction
389
+ OPTIONAL MATCH (peer)-[e1:HAS_ATTRIBUTE]->(attr:Attribute)-[e2:HAS_VALUE]->(peer_attr_val:AttributeValue)
390
+ WHERE (
391
+ (direction = "outbound" AND attr.name IN $outbound_rel_attr_map[rel_name])
392
+ OR (direction = "inbound" AND attr.name IN $inbound_rel_attr_map[rel_name])
393
+ OR (direction = "bidir" AND attr.name IN $bidirectional_rel_attr_map[rel_name])
394
+ )
395
+ AND e1.branch IN $branch_names
396
+ AND e1.to IS NULL
397
+ AND e1.status = "active"
398
+ AND e2.branch IN $branch_names
399
+ AND e2.to IS NULL
400
+ AND e2.status = "active"
401
+ // ------------
402
+ // collect everything to return a pair of lists with each node UUID
403
+ // ------------
404
+ WITH DISTINCT n, attr_vals_list, rel_name, peer, direction, attr.name AS peer_attr_name, peer_attr_val.value AS peer_val
405
+ WITH n, attr_vals_list, collect([rel_name, direction, peer_attr_name, peer_val]) AS peer_attr_vals_list
406
+ """ % {"schema_kind": self.schema_kind}
407
+ self.add_to_query(get_details_query)
408
+ self.return_labels = ["n.uuid AS n_uuid", "attr_vals_list", "peer_attr_vals_list"]
409
+
410
+
411
+ class UpdateAttributeValuesQuery(Query):
412
+ """
413
+ Update the values of the given attribute schema for the input Node-id-to-value map
414
+ Includes special handling for updating large-type attributes b/c they are not indexed and will be slow to update
415
+ on large data sets
416
+ """
417
+
418
+ name = "update_attribute_values"
419
+ type = QueryType.WRITE
420
+ insert_return = False
421
+
422
+ def __init__(self, attribute_schema: AttributeSchema, values_by_id_map: dict[str, Any], **kwargs: Any) -> None:
423
+ super().__init__(**kwargs)
424
+ self.attribute_name = attribute_schema.name
425
+ self.is_large_type_attribute = is_large_attribute_type(attribute_schema.kind)
426
+ self.is_branch_agnostic = attribute_schema.get_branch() is BranchSupportType.AGNOSTIC
427
+ self.values_by_id_map = values_by_id_map
428
+
429
+ async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
430
+ self.params = {
431
+ "node_uuids": list(self.values_by_id_map.keys()),
432
+ "attribute_name": self.attribute_name,
433
+ "values_by_id": self.values_by_id_map,
434
+ "default_branch": registry.default_branch,
435
+ "global_branch": GLOBAL_BRANCH_NAME,
436
+ "branch": GLOBAL_BRANCH_NAME if self.is_branch_agnostic else self.branch.name,
437
+ "branch_level": 1 if self.is_branch_agnostic else self.branch.hierarchy_level,
438
+ "at": self.at.to_string(),
439
+ }
440
+ branch_filter, branch_filter_params = self.branch.get_query_filter_path(at=self.at)
441
+ self.params.update(branch_filter_params)
442
+
443
+ if self.is_large_type_attribute:
444
+ # we make our own index of value to database ID, creating any vertices that are missing
445
+ # the mapping is a list of tuples instead of an actual mapping b/c creating an actual map is not possible
446
+ # without apoc functions
447
+ all_distinct_values = list(set(self.values_by_id_map.values()))
448
+ diy_index_query = """
449
+ MATCH (av:AttributeValue&!AttributeValueIndexed {is_default: false})
450
+ WHERE av.value IN $all_distinct_values
451
+ WITH collect([av.value, elementId(av)]) AS value_id_pairs, collect(av.value) AS found_values
452
+ WITH value_id_pairs, found_values,
453
+ reduce(
454
+ missing_distinct_values = [], value IN $all_distinct_values |
455
+ CASE
456
+ WHEN value IN found_values THEN missing_distinct_values
457
+ ELSE missing_distinct_values + [value]
458
+ END
459
+ ) AS missing_distinct_values
460
+ CALL (missing_distinct_values) {
461
+ UNWIND missing_distinct_values AS missing_value
462
+ CREATE (av:AttributeValue {is_default: false, value: missing_value})
463
+ RETURN collect([av.value, elementId(av)]) AS created_value_id_pairs
464
+ }
465
+ WITH value_id_pairs + created_value_id_pairs AS value_id_pairs
466
+ """
467
+ self.params["all_distinct_values"] = all_distinct_values
468
+ else:
469
+ # if this is not a large-type attribute, then just set the map to be empty
470
+ diy_index_query = """WITH [] AS value_id_pairs"""
471
+
472
+ self.add_to_query(diy_index_query)
473
+
474
+ if self.branch.name in [registry.default_branch, GLOBAL_BRANCH_NAME]:
475
+ update_value_query = """
476
+ // ------------
477
+ // Find the Nodes and Attributes we need to update
478
+ // ------------
479
+ MATCH (n:Node)-[e:IS_PART_OF]->(:Root)
480
+ WHERE n.uuid IN $node_uuids
481
+ AND e.branch IN [$default_branch, $global_branch]
482
+ AND e.to IS NULL
483
+ AND e.status = "active"
484
+ WITH DISTINCT n, value_id_pairs
485
+ MATCH (n)-[e:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
486
+ WHERE e.branch IN [$default_branch, $global_branch]
487
+ AND e.to IS NULL
488
+ AND e.status = "active"
489
+ // ------------
490
+ // If the attribute has an existing value on the branch, then set the to time on it
491
+ // but only if the value is different from the new value
492
+ // ------------
493
+ WITH DISTINCT n, attr, value_id_pairs
494
+ CALL (attr) {
495
+ OPTIONAL MATCH (attr)-[e:HAS_VALUE]->(existing_av)
496
+ WHERE e.branch IN [$default_branch, $global_branch]
497
+ AND e.to IS NULL
498
+ AND e.status = "active"
499
+ RETURN existing_av, e AS existing_has_value
500
+ }
501
+ CALL (existing_has_value) {
502
+ WITH existing_has_value
503
+ WHERE existing_has_value IS NOT NULL
504
+ SET existing_has_value.to = $at
505
+ }
506
+ """
507
+ else:
508
+ update_value_query = """
509
+ // ------------
510
+ // Find the Nodes and Attributes we need to update
511
+ // ------------
512
+ MATCH (n:Node)
513
+ WHERE n.uuid IN $node_uuids
514
+ CALL (n) {
515
+ MATCH (n)-[r:IS_PART_OF]->(:Root)
516
+ WHERE %(branch_filter)s
517
+ RETURN r.status = "active" AS is_active
518
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
519
+ LIMIT 1
520
+ }
521
+ WITH n, value_id_pairs, is_active
522
+ WHERE is_active = TRUE
523
+ WITH DISTINCT n, value_id_pairs
524
+ CALL (n) {
525
+ MATCH (n)-[r:HAS_ATTRIBUTE]->(attr:Attribute {name: $attribute_name})
526
+ WHERE %(branch_filter)s
527
+ RETURN attr, r.status = "active" AS is_active
528
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
529
+ LIMIT 1
530
+ }
531
+ WITH DISTINCT n, attr, value_id_pairs, is_active
532
+ WHERE is_active = TRUE
533
+ // ------------
534
+ // If the attribute has an existing value on the branch, then set the to time on it
535
+ // but only if the value is different from the new value
536
+ // ------------
537
+ CALL (n, attr) {
538
+ OPTIONAL MATCH (attr)-[r:HAS_VALUE]->(existing_av)
539
+ WHERE %(branch_filter)s
540
+ WITH r, existing_av
541
+ ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
542
+ LIMIT 1
543
+ WITH CASE
544
+ WHEN existing_av.value <> $values_by_id[n.uuid]
545
+ AND r.status = "active"
546
+ AND r.branch = $branch
547
+ THEN [r, existing_av]
548
+ ELSE [NULL, NULL]
549
+ END AS existing_details
550
+ RETURN existing_details[0] AS existing_has_value, existing_details[1] AS existing_av
551
+ }
552
+ CALL (existing_has_value) {
553
+ WITH existing_has_value
554
+ WHERE existing_has_value IS NOT NULL
555
+ SET existing_has_value.to = $at
556
+ }
557
+ """ % {"branch_filter": branch_filter}
558
+ self.add_to_query(update_value_query)
559
+
560
+ if self.is_large_type_attribute:
561
+ # use the index we created at the start to get the database ID of the AttributeValue vertex
562
+ # and then link the Attribute to the AttributeValue
563
+ set_value_query = """
564
+ // ------------
565
+ // only make updates if the existing value is not the same as the new value
566
+ // ------------
567
+ WITH attr, existing_av, value_id_pairs, $values_by_id[n.uuid] AS required_value
568
+ WHERE existing_av.value <> required_value
569
+ OR existing_av IS NULL
570
+ WITH attr, value_id_pairs, required_value,
571
+ reduce(av_vertex_id = NULL, pair IN value_id_pairs |
572
+ CASE
573
+ WHEN av_vertex_id IS NOT NULL THEN av_vertex_id
574
+ WHEN pair[0] = required_value THEN pair[1]
575
+ ELSE av_vertex_id
576
+ END
577
+ ) AS av_vertex_id
578
+ MATCH (av:AttributeValue)
579
+ WHERE elementId(av) = av_vertex_id
580
+ CREATE (attr)-[r:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
581
+ """
582
+ else:
583
+ # if not a large-type attribute, then we can just use the regular MERGE clause
584
+ # that makes use of the index on AttributeValueIndexed
585
+ set_value_query = """
586
+ // ------------
587
+ // only make updates if the existing value is not the same as the new value
588
+ // ------------
589
+ WITH n, attr, existing_av, value_id_pairs, $values_by_id[n.uuid] AS required_value
590
+ WHERE existing_av.value <> required_value
591
+ OR existing_av IS NULL
592
+ CALL (n, attr) {
593
+ MERGE (av:AttributeValue&AttributeValueIndexed {is_default: false, value: $values_by_id[n.uuid]} )
594
+ WITH av, attr
595
+ LIMIT 1
596
+ CREATE (attr)-[r:HAS_VALUE { branch: $branch, branch_level: $branch_level, status: "active", from: $at }]->(av)
597
+ }
598
+ """
599
+ self.add_to_query(set_value_query)
600
+
601
+
602
+ class Migration043(MigrationRequiringRebase):
603
+ """
604
+ Backfill `human_friendly_id` and `display_label` attributes for nodes with schemas that define them.
605
+ """
606
+
607
+ name: str = "043_backfill_hfid_display_label_in_db"
608
+ minimum_version: int = 42
609
+ update_batch_size: int = 1000
610
+ # skip these b/c the attributes on these schema-related nodes are used to define the values included in
611
+ # the human_friendly_id and display_label attributes on instances of these schema, so should not be updated
612
+ kinds_to_skip: list[str] = [
613
+ "SchemaNode",
614
+ "SchemaAttribute",
615
+ "SchemaRelationship",
616
+ "SchemaGeneric",
617
+ ]
618
+
619
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
620
+ return MigrationResult()
621
+
622
+ async def _do_one_schema_all(
623
+ self,
624
+ db: InfrahubDatabase,
625
+ branch: Branch,
626
+ schema: NodeSchema,
627
+ schema_branch: SchemaBranch,
628
+ attribute_schema_map: dict[AttributeSchema, AttributeSchema],
629
+ progress: Progress | None = None,
630
+ update_task: TaskID | None = None,
631
+ ) -> None:
632
+ print(f"Processing {schema.kind}...", end="")
633
+
634
+ schema_paths_by_name: dict[str, list[SchemaAttributePath]] = {}
635
+ for source_attribute_schema in attribute_schema_map.keys():
636
+ node_schema_property = getattr(schema, source_attribute_schema.name)
637
+ if not node_schema_property:
638
+ continue
639
+ if isinstance(node_schema_property, list):
640
+ schema_paths_by_name[source_attribute_schema.name] = [
641
+ schema.parse_schema_path(path=str(path), schema=schema_branch) for path in node_schema_property
642
+ ]
643
+ else:
644
+ schema_paths_by_name[source_attribute_schema.name] = [
645
+ schema.parse_schema_path(path=str(node_schema_property), schema=schema_branch)
646
+ ]
647
+ all_schema_paths = list(chain(*schema_paths_by_name.values()))
648
+ offset = 0
649
+
650
+ # loop until we get no results from the get_details_query
651
+ while True:
652
+ if branch.is_default:
653
+ get_details_query: GetResultMapQuery = await GetPathDetailsDefaultBranch.init(
654
+ db=db,
655
+ schema_kind=schema.kind,
656
+ schema_paths=all_schema_paths,
657
+ offset=offset,
658
+ limit=self.update_batch_size,
659
+ )
660
+ else:
661
+ get_details_query = await GetPathDetailsBranchQuery.init(
662
+ db=db,
663
+ branch=branch,
664
+ schema_kind=schema.kind,
665
+ schema_paths=all_schema_paths,
666
+ updates_only=False,
667
+ offset=offset,
668
+ limit=self.update_batch_size,
669
+ )
670
+ await get_details_query.execute(db=db)
671
+
672
+ num_updates = 0
673
+ for source_attribute_schema, destination_attribute_schema in attribute_schema_map.items():
674
+ schema_paths = schema_paths_by_name[source_attribute_schema.name]
675
+ schema_path_values_map = get_details_query.get_result_map(schema_paths)
676
+ num_updates = max(num_updates, len(schema_path_values_map))
677
+ formatted_schema_path_values_map = {}
678
+ for k, v in schema_path_values_map.items():
679
+ if not v:
680
+ continue
681
+ if destination_attribute_schema.kind == "List":
682
+ formatted_schema_path_values_map[k] = ujson.dumps(v)
683
+ else:
684
+ formatted_schema_path_values_map[k] = " ".join(item for item in v if item is not None)
685
+
686
+ if not formatted_schema_path_values_map:
687
+ continue
688
+
689
+ update_display_label_query = await UpdateAttributeValuesQuery.init(
690
+ db=db,
691
+ branch=branch,
692
+ attribute_schema=destination_attribute_schema,
693
+ values_by_id_map=formatted_schema_path_values_map,
694
+ )
695
+ await update_display_label_query.execute(db=db)
696
+
697
+ if progress is not None and update_task is not None:
698
+ progress.update(update_task, advance=num_updates)
699
+
700
+ if num_updates == 0:
701
+ break
702
+
703
+ offset += self.update_batch_size
704
+
705
+ print("done")
706
+
707
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
708
+ root_node = await get_root_node(db=db, initialize=False)
709
+ default_branch_name = root_node.default_branch
710
+ default_branch = await Branch.get_by_name(db=db, name=default_branch_name)
711
+
712
+ main_schema_branch = await get_or_load_schema_branch(db=db, branch=default_branch)
713
+
714
+ total_nodes_query = await DefaultBranchNodeCount.init(db=db, kinds_to_skip=self.kinds_to_skip)
715
+ await total_nodes_query.execute(db=db)
716
+ total_nodes_count = total_nodes_query.get_num_nodes()
717
+
718
+ base_node_schema = main_schema_branch.get("SchemaNode", duplicate=False)
719
+ display_label_attribute_schema = base_node_schema.get_attribute("display_label")
720
+ display_labels_attribute_schema = base_node_schema.get_attribute("display_labels")
721
+ hfid_attribute_schema = base_node_schema.get_attribute("human_friendly_id")
722
+
723
+ try:
724
+ with Progress() as progress:
725
+ update_task = progress.add_task(
726
+ f"Set display_label and human_friendly_id for {total_nodes_count} nodes on default branch",
727
+ total=total_nodes_count,
728
+ )
729
+ for node_schema_name in main_schema_branch.node_names:
730
+ if node_schema_name in self.kinds_to_skip:
731
+ continue
732
+
733
+ node_schema = main_schema_branch.get_node(name=node_schema_name, duplicate=False)
734
+ attribute_schema_map = {}
735
+ if node_schema.display_labels:
736
+ attribute_schema_map[display_labels_attribute_schema] = display_label_attribute_schema
737
+ if node_schema.human_friendly_id:
738
+ attribute_schema_map[hfid_attribute_schema] = hfid_attribute_schema
739
+ if not attribute_schema_map:
740
+ continue
741
+
742
+ await self._do_one_schema_all(
743
+ db=db,
744
+ branch=default_branch,
745
+ schema=node_schema,
746
+ schema_branch=main_schema_branch,
747
+ attribute_schema_map=attribute_schema_map,
748
+ progress=progress,
749
+ update_task=update_task,
750
+ )
751
+
752
+ except Exception as exc:
753
+ return MigrationResult(errors=[str(exc)])
754
+ return MigrationResult()
755
+
756
+ async def _do_one_schema_branch(
757
+ self,
758
+ db: InfrahubDatabase,
759
+ branch: Branch,
760
+ schema: NodeSchema,
761
+ schema_branch: SchemaBranch,
762
+ source_attribute_schema: AttributeSchema,
763
+ destination_attribute_schema: AttributeSchema,
764
+ ) -> None:
765
+ print(f"Processing {schema.kind}.{destination_attribute_schema.name} for {branch.name}...", end="")
766
+
767
+ schema_property = getattr(schema, source_attribute_schema.name)
768
+ if isinstance(schema_property, list):
769
+ schema_paths = [
770
+ schema.parse_schema_path(path=str(path_part), schema=schema_branch) for path_part in schema_property
771
+ ]
772
+ else:
773
+ schema_paths = [schema.parse_schema_path(path=str(schema_property), schema=schema_branch)]
774
+
775
+ offset = 0
776
+
777
+ while True:
778
+ # loop until we get no results from the get_details_query
779
+ get_details_query = await GetPathDetailsBranchQuery.init(
780
+ db=db,
781
+ branch=branch,
782
+ schema_kind=schema.kind,
783
+ schema_paths=schema_paths,
784
+ offset=offset,
785
+ limit=self.update_batch_size,
786
+ )
787
+ await get_details_query.execute(db=db)
788
+
789
+ schema_path_values_map = get_details_query.get_result_map(schema_paths)
790
+ if not schema_path_values_map:
791
+ print("done")
792
+ break
793
+ formatted_schema_path_values_map = {}
794
+ for k, v in schema_path_values_map.items():
795
+ if not v:
796
+ continue
797
+ if destination_attribute_schema.kind == "List":
798
+ formatted_v = ujson.dumps(v)
799
+ else:
800
+ formatted_v = " ".join(item for item in v if item is not None)
801
+ formatted_schema_path_values_map[k] = formatted_v
802
+
803
+ update_attr_values_query = await UpdateAttributeValuesQuery.init(
804
+ db=db,
805
+ branch=branch,
806
+ attribute_schema=destination_attribute_schema,
807
+ values_by_id_map=formatted_schema_path_values_map,
808
+ )
809
+ await update_attr_values_query.execute(db=db)
810
+
811
+ offset += self.update_batch_size
812
+
813
+ async def execute_against_branch(self, db: InfrahubDatabase, branch: Branch) -> MigrationResult:
814
+ default_branch = await Branch.get_by_name(db=db, name=registry.default_branch)
815
+ main_schema_branch = await get_or_load_schema_branch(db=db, branch=default_branch)
816
+ schema_branch = await get_or_load_schema_branch(db=db, branch=branch)
817
+
818
+ base_node_schema = schema_branch.get("SchemaNode", duplicate=False)
819
+ display_label_attribute_schema = base_node_schema.get_attribute("display_label")
820
+ display_labels_attribute_schema = base_node_schema.get_attribute("display_labels")
821
+ hfid_attribute_schema = base_node_schema.get_attribute("human_friendly_id")
822
+
823
+ try:
824
+ for node_schema_name in schema_branch.node_names:
825
+ if node_schema_name in self.kinds_to_skip:
826
+ continue
827
+
828
+ node_schema = schema_branch.get_node(name=node_schema_name, duplicate=False)
829
+ default_node_schema = main_schema_branch.get_node(name=node_schema_name, duplicate=False)
830
+ schemas_for_universal_update_map = {}
831
+ schemas_for_targeted_update_map = {}
832
+ if default_node_schema.display_label != node_schema.display_label:
833
+ schemas_for_universal_update_map[display_labels_attribute_schema] = display_label_attribute_schema
834
+ elif node_schema.display_labels:
835
+ schemas_for_targeted_update_map[display_labels_attribute_schema] = display_label_attribute_schema
836
+
837
+ if default_node_schema.human_friendly_id != node_schema.human_friendly_id:
838
+ schemas_for_universal_update_map[hfid_attribute_schema] = hfid_attribute_schema
839
+ elif node_schema.human_friendly_id:
840
+ schemas_for_targeted_update_map[hfid_attribute_schema] = hfid_attribute_schema
841
+
842
+ if schemas_for_universal_update_map:
843
+ await self._do_one_schema_all(
844
+ db=db,
845
+ branch=branch,
846
+ schema=node_schema,
847
+ schema_branch=schema_branch,
848
+ attribute_schema_map=schemas_for_universal_update_map,
849
+ )
850
+
851
+ if not schemas_for_targeted_update_map:
852
+ continue
853
+
854
+ for source_attribute_schema, destination_attribute_schema in schemas_for_targeted_update_map.items():
855
+ await self._do_one_schema_branch(
856
+ db=db,
857
+ branch=branch,
858
+ schema=node_schema,
859
+ schema_branch=schema_branch,
860
+ source_attribute_schema=source_attribute_schema,
861
+ destination_attribute_schema=destination_attribute_schema,
862
+ )
863
+
864
+ except Exception as exc:
865
+ return MigrationResult(errors=[str(exc)])
866
+ return MigrationResult()