Skip to content

Commit c67530f

Browse files
authored
Merge pull request #7450 from opsmill/stable-to-release-1.5
Stable to release 1.5
2 parents e1bc87b + e0334c5 commit c67530f

File tree

14 files changed

+740
-439
lines changed

14 files changed

+740
-439
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
GRAPH_VERSION = 42
1+
GRAPH_VERSION = 43

backend/infrahub/core/migrations/graph/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@
4141
from .m037_index_attr_vals import Migration037
4242
from .m038_redo_0000_prefix_fix import Migration038
4343
from .m039_ipam_reconcile import Migration039
44-
from .m040_profile_attrs_in_db import Migration040
45-
from .m041_create_hfid_display_label_in_db import Migration041
46-
from .m042_backfill_hfid_display_label_in_db import Migration042
44+
from .m040_duplicated_attributes import Migration040
45+
from .m041_profile_attrs_in_db import Migration041
46+
from .m042_create_hfid_display_label_in_db import Migration042
47+
from .m043_backfill_hfid_display_label_in_db import Migration043
4748

4849
if TYPE_CHECKING:
4950
from infrahub.core.root import Root
@@ -93,6 +94,7 @@
9394
Migration040,
9495
Migration041,
9596
Migration042,
97+
Migration043,
9698
]
9799

98100

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any, Sequence
4+
5+
from infrahub.core.migrations.shared import MigrationResult
6+
from infrahub.core.query import Query, QueryType
7+
8+
from ..shared import GraphMigration
9+
10+
if TYPE_CHECKING:
11+
from infrahub.database import InfrahubDatabase
12+
13+
14+
class DeleteDuplicatedAttributesQuery(Query):
15+
name: str = "delete_duplicated_attributes"
16+
type: QueryType = QueryType.WRITE
17+
insert_return: bool = False
18+
insert_limit: bool = False
19+
20+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
21+
query = """
22+
// -------------
23+
// get all the Nodes linked to multiple Attributes with the same name to drastically reduce the search space
24+
// -------------
25+
MATCH (n:Node)-[:HAS_ATTRIBUTE]->(attr:Attribute)
26+
WITH DISTINCT n, attr
27+
WITH n, attr.name AS attr_name, count(*) AS num_attrs
28+
WHERE num_attrs > 1
29+
// -------------
30+
// for each Node-attr_name pair, get the possible duplicate Attributes
31+
// -------------
32+
MATCH (n)-[:HAS_ATTRIBUTE]->(dup_attr:Attribute {name: attr_name})
33+
WITH DISTINCT n, dup_attr
34+
// -------------
35+
// get the branch(es) for each possible duplicate Attribute
36+
// -------------
37+
CALL (n, dup_attr) {
38+
MATCH (n)-[r:HAS_ATTRIBUTE {status: "active"}]->(dup_attr)
39+
WHERE r.to IS NULL
40+
AND NOT exists((n)-[:HAS_ATTRIBUTE {status: "deleted", branch: r.branch}]->(dup_attr))
41+
RETURN r.branch AS branch
42+
}
43+
// -------------
44+
// get the latest update time for each duplicate Attribute on each branch
45+
// -------------
46+
CALL (dup_attr, branch) {
47+
MATCH (dup_attr)-[r {branch: branch}]-()
48+
RETURN max(r.from) AS latest_update
49+
}
50+
// -------------
51+
// order the duplicate Attributes by latest update time
52+
// -------------
53+
WITH n, dup_attr, branch, latest_update
54+
ORDER BY n, branch, dup_attr.name, latest_update DESC
55+
// -------------
56+
// for any Node-dup_attr_name pairs with multiple duplicate Attributes, keep the Attribute with the latest update
57+
// on this branch and delete all the other edges on this branch for this Attribute
58+
// -------------
59+
WITH n, branch, dup_attr.name AS dup_attr_name, collect(dup_attr) AS dup_attrs_reverse_chronological
60+
WHERE size(dup_attrs_reverse_chronological) > 1
61+
WITH branch, tail(dup_attrs_reverse_chronological) AS dup_attrs_to_delete
62+
UNWIND dup_attrs_to_delete AS dup_attr_to_delete
63+
MATCH (dup_attr_to_delete)-[r {branch: branch}]-()
64+
DELETE r
65+
// -------------
66+
// delete any orphaned Attributes
67+
// -------------
68+
WITH DISTINCT dup_attr_to_delete
69+
WHERE NOT exists((dup_attr_to_delete)--())
70+
DELETE dup_attr_to_delete
71+
"""
72+
self.add_to_query(query)
73+
74+
75+
class Migration040(GraphMigration):
76+
name: str = "040_duplicated_attributes"
77+
queries: Sequence[type[Query]] = [DeleteDuplicatedAttributesQuery]
78+
minimum_version: int = 39
79+
80+
async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult: # noqa: ARG002
81+
return MigrationResult()

backend/infrahub/core/migrations/graph/m040_profile_attrs_in_db.py renamed to backend/infrahub/core/migrations/graph/m041_profile_attrs_in_db.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def get_node_ids_by_branch(self) -> dict[str, set[str]]:
8484
return nodes_by_branch
8585

8686

87-
class Migration040(ArbitraryMigration):
87+
class Migration041(ArbitraryMigration):
8888
"""
8989
Save profile attribute values on each node using the profile in the database
9090
For any profile that has updates on a given branch (including default branch)
@@ -93,8 +93,8 @@ class Migration040(ArbitraryMigration):
9393
- run NodeProfilesApplier.apply_profiles on the node on that branch
9494
"""
9595

96-
name: str = "040_profile_attrs_in_db"
97-
minimum_version: int = 39
96+
name: str = "041_profile_attrs_in_db"
97+
minimum_version: int = 40
9898

9999
def __init__(self, *args: Any, **kwargs: Any) -> None:
100100
super().__init__(*args, **kwargs)

backend/infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py renamed to backend/infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
from infrahub.database import InfrahubDatabase
1818

1919

20-
class Migration041(InternalSchemaMigration):
21-
name: str = "041_create_hfid_display_label_in_db"
22-
minimum_version: int = 40
20+
class Migration042(InternalSchemaMigration):
21+
name: str = "042_create_hfid_display_label_in_db"
22+
minimum_version: int = 41
2323

2424
@classmethod
2525
def init(cls, **kwargs: Any) -> Self:

backend/infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py renamed to backend/infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
from infrahub.database import InfrahubDatabase
1919

2020

21-
class Migration042(ArbitraryMigration):
21+
class Migration043(ArbitraryMigration):
2222
"""
2323
Backfill `human_friendly_id` and `display_label` attributes for nodes with schemas that define them.
2424
"""
2525

26-
name: str = "042_backfill_hfid_display_label_in_db"
27-
minimum_version: int = 41
26+
name: str = "043_backfill_hfid_display_label_in_db"
27+
minimum_version: int = 42
2828

2929
def __init__(self, *args: Any, **kwargs: Any) -> None:
3030
super().__init__(*args, **kwargs)

backend/infrahub/core/migrations/schema/node_attribute_add.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ class NodeAttributeAddMigration(AttributeSchemaMigration):
4242
name: str = "node.attribute.add"
4343
queries: Sequence[type[AttributeMigrationQuery]] = [NodeAttributeAddMigrationQuery01] # type: ignore[assignment]
4444

45+
async def execute(
46+
self,
47+
db: InfrahubDatabase,
48+
branch: Branch,
49+
at: Timestamp | str | None = None,
50+
) -> MigrationResult:
51+
if self.new_attribute_schema.inherited is True:
52+
return MigrationResult()
53+
return await super().execute(db=db, branch=branch, at=at)
54+
4555
async def execute_post_queries(
4656
self,
4757
db: InfrahubDatabase,

backend/infrahub/core/query/node.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,15 @@ async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG
246246
ipnetwork_prop_list = [f"{key}: {value}" for key, value in ipnetwork_prop.items()]
247247

248248
attrs_nonindexed_query = """
249-
WITH distinct n
249+
WITH DISTINCT n
250250
UNWIND $attrs AS attr
251251
// Try to find a matching vertex
252-
OPTIONAL MATCH (existing_av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
253-
WHERE NOT existing_av:AttributeValueIndexed
252+
CALL (attr) {
253+
OPTIONAL MATCH (existing_av:AttributeValue {value: attr.content.value, is_default: attr.content.is_default})
254+
WHERE NOT existing_av:AttributeValueIndexed
255+
RETURN existing_av
256+
LIMIT 1
257+
}
254258
CALL (attr, existing_av) {
255259
// If none found, create a new one
256260
WITH existing_av

backend/tests/helpers/db_validation.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,38 @@ async def verify_no_duplicate_paths(db: InfrahubDatabase) -> None:
130130
raise ValueError(
131131
f"{num_paths} paths ({branch=},{edge_type=},{from_time=}) between nodes '{node_id1}' and '{node_id2}'"
132132
)
133+
134+
135+
async def validate_no_duplicate_attributes(db: InfrahubDatabase, branch: Branch) -> list[str]:
136+
"""
137+
Validate that no Nodes have duplicated attribute or relationship names
138+
"""
139+
branch_filter, branch_params = branch.get_query_filter_path()
140+
141+
query = """
142+
// -------------
143+
// get all the active Attributes this branch and count them up
144+
// -------------
145+
MATCH (n:Node)-[:HAS_ATTRIBUTE]->(field:Attribute)
146+
WITH DISTINCT n, field
147+
CALL (n, field) {
148+
MATCH (n)-[r:HAS_ATTRIBUTE]->(field)
149+
WHERE %(branch_filter)s
150+
RETURN r
151+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
152+
LIMIT 1
153+
}
154+
WITH n, field, r
155+
WHERE r.status = "active" AND r.to IS NULL
156+
WITH n.uuid AS node_id, field.name AS field_name, count(*) AS num_fields
157+
WHERE num_fields > 1
158+
RETURN node_id, field_name, num_fields
159+
""" % {"branch_filter": branch_filter}
160+
results = await db.execute_query(query=query, params=branch_params)
161+
errors = []
162+
for result in results:
163+
node_id = result.get("node_id")
164+
field_name = result.get("field_name")
165+
num_fields = result.get("num_fields")
166+
errors.append(f"Node '{node_id}' has {num_fields} duplicated attributes with {field_name=}")
167+
return errors

0 commit comments

Comments
 (0)