infrahub-server 1.2.7__py3-none-any.whl → 1.2.9__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 (85) hide show
  1. infrahub/api/transformation.py +1 -0
  2. infrahub/artifacts/models.py +4 -0
  3. infrahub/cli/db.py +15 -6
  4. infrahub/computed_attribute/tasks.py +34 -12
  5. infrahub/config.py +2 -1
  6. infrahub/constants/__init__.py +0 -0
  7. infrahub/core/branch/tasks.py +0 -2
  8. infrahub/core/constants/__init__.py +1 -0
  9. infrahub/core/diff/calculator.py +4 -3
  10. infrahub/core/diff/combiner.py +1 -2
  11. infrahub/core/diff/coordinator.py +44 -28
  12. infrahub/core/diff/data_check_synchronizer.py +3 -2
  13. infrahub/core/diff/enricher/hierarchy.py +38 -27
  14. infrahub/core/diff/ipam_diff_parser.py +5 -4
  15. infrahub/core/diff/merger/merger.py +20 -18
  16. infrahub/core/diff/model/field_specifiers_map.py +64 -0
  17. infrahub/core/diff/model/path.py +55 -58
  18. infrahub/core/diff/parent_node_adder.py +14 -16
  19. infrahub/core/diff/query/drop_nodes.py +42 -0
  20. infrahub/core/diff/query/field_specifiers.py +8 -7
  21. infrahub/core/diff/query/filters.py +15 -1
  22. infrahub/core/diff/query/save.py +3 -0
  23. infrahub/core/diff/query_parser.py +49 -52
  24. infrahub/core/diff/repository/deserializer.py +36 -23
  25. infrahub/core/diff/repository/repository.py +31 -12
  26. infrahub/core/graph/__init__.py +1 -1
  27. infrahub/core/graph/index.py +3 -1
  28. infrahub/core/initialization.py +23 -7
  29. infrahub/core/manager.py +16 -5
  30. infrahub/core/migrations/graph/__init__.py +2 -0
  31. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +9 -8
  32. infrahub/core/migrations/graph/m027_delete_isolated_nodes.py +50 -0
  33. infrahub/core/protocols.py +1 -0
  34. infrahub/core/query/branch.py +27 -17
  35. infrahub/core/query/diff.py +65 -38
  36. infrahub/core/query/node.py +111 -33
  37. infrahub/core/query/relationship.py +17 -3
  38. infrahub/core/query/subquery.py +2 -2
  39. infrahub/core/schema/definitions/core/builtin.py +2 -4
  40. infrahub/core/schema/definitions/core/transform.py +1 -0
  41. infrahub/core/schema/schema_branch.py +3 -0
  42. infrahub/core/validators/aggregated_checker.py +2 -2
  43. infrahub/core/validators/uniqueness/query.py +30 -9
  44. infrahub/database/__init__.py +1 -16
  45. infrahub/database/index.py +1 -1
  46. infrahub/database/memgraph.py +1 -12
  47. infrahub/database/neo4j.py +1 -13
  48. infrahub/git/integrator.py +27 -3
  49. infrahub/git/models.py +4 -0
  50. infrahub/git/tasks.py +3 -0
  51. infrahub/git_credential/helper.py +2 -2
  52. infrahub/graphql/mutations/computed_attribute.py +5 -1
  53. infrahub/graphql/queries/diff/tree.py +2 -1
  54. infrahub/message_bus/operations/requests/proposed_change.py +6 -0
  55. infrahub/message_bus/types.py +3 -0
  56. infrahub/patch/queries/consolidate_duplicated_nodes.py +109 -0
  57. infrahub/patch/queries/delete_duplicated_edges.py +138 -0
  58. infrahub/proposed_change/tasks.py +1 -0
  59. infrahub/server.py +1 -3
  60. infrahub/transformations/models.py +3 -0
  61. infrahub/transformations/tasks.py +1 -0
  62. infrahub/trigger/models.py +11 -1
  63. infrahub/trigger/setup.py +38 -13
  64. infrahub/trigger/tasks.py +1 -4
  65. infrahub/webhook/models.py +3 -0
  66. infrahub/workflows/initialization.py +1 -3
  67. infrahub_sdk/client.py +4 -4
  68. infrahub_sdk/config.py +17 -0
  69. infrahub_sdk/ctl/cli_commands.py +7 -1
  70. infrahub_sdk/ctl/generator.py +2 -2
  71. infrahub_sdk/generator.py +12 -66
  72. infrahub_sdk/operation.py +80 -0
  73. infrahub_sdk/protocols.py +12 -0
  74. infrahub_sdk/recorder.py +3 -0
  75. infrahub_sdk/schema/repository.py +4 -0
  76. infrahub_sdk/transforms.py +15 -27
  77. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/METADATA +2 -2
  78. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/RECORD +84 -78
  79. infrahub_testcontainers/container.py +1 -0
  80. infrahub_testcontainers/docker-compose.test.yml +5 -1
  81. infrahub/database/manager.py +0 -15
  82. /infrahub/{database/constants.py → constants/database.py} +0 -0
  83. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/LICENSE.txt +0 -0
  84. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/WHEEL +0 -0
  85. {infrahub_server-1.2.7.dist-info → infrahub_server-1.2.9.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,10 @@
1
+ from collections import defaultdict
2
+ from typing import Any
3
+
1
4
  from pydantic import BaseModel, Field
2
5
 
3
6
  from infrahub.core.constants import DiffAction
7
+ from infrahub.core.diff.model.path import NodeIdentifier
4
8
  from infrahub.core.query.utils import filter_and, filter_or
5
9
 
6
10
 
@@ -29,6 +33,7 @@ class IncExclActionFilterOptions(BaseModel):
29
33
  class EnrichedDiffQueryFilters(BaseModel):
30
34
  ids: list[str] = Field(default_factory=list)
31
35
  kind: IncExclFilterOptions = IncExclFilterOptions()
36
+ identifiers: list[NodeIdentifier] = Field(default_factory=list)
32
37
  namespace: IncExclFilterOptions = IncExclFilterOptions()
33
38
  status: IncExclActionFilterOptions = IncExclActionFilterOptions()
34
39
  only_conflicted: bool = Field(default=False)
@@ -37,6 +42,7 @@ class EnrichedDiffQueryFilters(BaseModel):
37
42
  def is_empty(self) -> bool:
38
43
  if (
39
44
  not self.ids
45
+ and not self.identifiers
40
46
  and self.only_conflicted is False
41
47
  and self.kind.is_empty
42
48
  and self.namespace.is_empty
@@ -48,12 +54,20 @@ class EnrichedDiffQueryFilters(BaseModel):
48
54
  def generate(self) -> tuple[str, dict]:
49
55
  default_filter = ""
50
56
 
51
- params = {}
57
+ params: dict[str, Any] = {}
52
58
 
53
59
  if self.ids:
54
60
  params["ids"] = self.ids
55
61
  return "diff_node.uuid in $ids", params
56
62
 
63
+ if self.identifiers:
64
+ params["ids"] = [n.uuid for n in self.identifiers]
65
+ id_kind_map: dict[str, list[str]] = defaultdict(list)
66
+ for node_identifier in self.identifiers:
67
+ id_kind_map[node_identifier.uuid].append(node_identifier.kind)
68
+ params["id_kind_map"] = id_kind_map
69
+ return "diff_node.uuid in $ids AND diff_node.kind IN $id_kind_map[diff_node.uuid]", params
70
+
57
71
  filters_list = []
58
72
 
59
73
  if self.is_empty:
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from typing import Any, Iterable
2
3
 
3
4
  from infrahub.core.query import Query, QueryType
@@ -92,6 +93,7 @@ WITH root_uuid, node_map, diff_node, (node_map.conflict_params IS NOT NULL) AS h
92
93
  SET
93
94
  diff_node.kind = node_map.node_properties.kind,
94
95
  diff_node.label = node_map.node_properties.label,
96
+ diff_node.db_labels = node_map.node_properties.db_labels,
95
97
  diff_node.changed_at = node_map.node_properties.changed_at,
96
98
  diff_node.action = node_map.node_properties.action,
97
99
  diff_node.path_identifier = node_map.node_properties.path_identifier
@@ -401,6 +403,7 @@ FOREACH (i in CASE WHEN has_property_conflict = TRUE THEN [1] ELSE [] END |
401
403
  "node_properties": {
402
404
  "uuid": enriched_node.uuid,
403
405
  "kind": enriched_node.kind,
406
+ "db_labels": json.dumps(list(enriched_node.identifier.labels)),
404
407
  "label": enriched_node.label,
405
408
  "changed_at": enriched_node.changed_at.to_string() if enriched_node.changed_at else None,
406
409
  "action": enriched_node.action.value,
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections import defaultdict
4
3
  from dataclasses import dataclass, field
5
4
  from typing import TYPE_CHECKING, Any
6
5
  from uuid import uuid4
@@ -15,6 +14,7 @@ from infrahub.core.constants import (
15
14
  from infrahub.core.constants.database import DatabaseEdgeType
16
15
  from infrahub.core.timestamp import Timestamp
17
16
 
17
+ from .model.field_specifiers_map import NodeFieldSpecifierMap
18
18
  from .model.path import (
19
19
  DatabasePath,
20
20
  DiffAttribute,
@@ -23,6 +23,7 @@ from .model.path import (
23
23
  DiffRelationship,
24
24
  DiffRoot,
25
25
  DiffSingleRelationship,
26
+ NodeIdentifier,
26
27
  )
27
28
 
28
29
  if TYPE_CHECKING:
@@ -394,8 +395,7 @@ class DiffRelationshipIntermediate:
394
395
  @dataclass
395
396
  class DiffNodeIntermediate(TrackedStatusUpdates):
396
397
  force_action: DiffAction | None
397
- uuid: str
398
- kind: str
398
+ identifier: NodeIdentifier
399
399
  db_id: str
400
400
  from_time: Timestamp
401
401
  status: RelationshipStatus
@@ -403,6 +403,14 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
403
403
  # {(name, identifier): DiffRelationshipIntermediate}
404
404
  relationships_by_identifier: dict[tuple[str, str], DiffRelationshipIntermediate] = field(default_factory=dict)
405
405
 
406
+ @property
407
+ def uuid(self) -> str:
408
+ return self.identifier.uuid
409
+
410
+ @property
411
+ def kind(self) -> str:
412
+ return self.identifier.kind
413
+
406
414
  def to_diff_node(self, from_time: Timestamp, include_unchanged: bool) -> DiffNode:
407
415
  attributes = []
408
416
  for attr in self.attributes_by_name.values():
@@ -424,8 +432,7 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
424
432
  if self.force_action:
425
433
  action = self.force_action
426
434
  return DiffNode(
427
- uuid=self.uuid,
428
- kind=self.kind,
435
+ identifier=self.identifier,
429
436
  changed_at=changed_at,
430
437
  action=action,
431
438
  attributes=attributes,
@@ -441,11 +448,11 @@ class DiffNodeIntermediate(TrackedStatusUpdates):
441
448
  class DiffRootIntermediate:
442
449
  uuid: str
443
450
  branch: str
444
- nodes_by_id: dict[str, DiffNodeIntermediate] = field(default_factory=dict)
451
+ nodes_by_identifier: dict[NodeIdentifier, DiffNodeIntermediate] = field(default_factory=dict)
445
452
 
446
453
  def to_diff_root(self, from_time: Timestamp, to_time: Timestamp, include_unchanged: bool) -> DiffRoot:
447
454
  nodes = []
448
- for node in self.nodes_by_id.values():
455
+ for node in self.nodes_by_identifier.values():
449
456
  if node.is_empty:
450
457
  continue
451
458
  diff_node = node.to_diff_node(from_time=from_time, include_unchanged=include_unchanged)
@@ -462,7 +469,7 @@ class DiffQueryParser:
462
469
  schema_manager: SchemaManager,
463
470
  from_time: Timestamp,
464
471
  to_time: Timestamp | None = None,
465
- previous_node_field_specifiers: dict[str, set[str]] | None = None,
472
+ previous_node_field_specifiers: NodeFieldSpecifierMap | None = None,
466
473
  ) -> None:
467
474
  self.base_branch_name = base_branch.name
468
475
  self.diff_branch_name = diff_branch.name
@@ -476,9 +483,9 @@ class DiffQueryParser:
476
483
  self.diff_branched_from_time = Timestamp(diff_branch.get_branched_from())
477
484
  self._diff_root_by_branch: dict[str, DiffRootIntermediate] = {}
478
485
  self._final_diff_root_by_branch: dict[str, DiffRoot] = {}
479
- self._previous_node_field_specifiers = previous_node_field_specifiers or {}
480
- self._new_node_field_specifiers: dict[str, set[str]] | None = None
481
- self._current_node_field_specifiers: dict[str, set[str]] | None = None
486
+ self._previous_node_field_specifiers = previous_node_field_specifiers or NodeFieldSpecifierMap()
487
+ self._new_node_field_specifiers: NodeFieldSpecifierMap | None = None
488
+ self._current_node_field_specifiers: NodeFieldSpecifierMap | None = None
482
489
 
483
490
  def get_branches(self) -> set[str]:
484
491
  return set(self._final_diff_root_by_branch.keys())
@@ -490,48 +497,33 @@ class DiffQueryParser:
490
497
  return self._final_diff_root_by_branch[branch]
491
498
  return DiffRoot(from_time=self.from_time, to_time=self.to_time, uuid=str(uuid4()), branch=branch, nodes=[])
492
499
 
493
- def get_diff_node_field_specifiers(self) -> dict[str, set[str]]:
500
+ def get_diff_node_field_specifiers(self) -> NodeFieldSpecifierMap:
501
+ node_field_specifiers_map = NodeFieldSpecifierMap()
494
502
  if self.diff_branch_name not in self._diff_root_by_branch:
495
- return {}
496
- node_field_specifiers_map: dict[str, set[str]] = defaultdict(set)
503
+ return node_field_specifiers_map
497
504
  diff_root = self._diff_root_by_branch[self.diff_branch_name]
498
- for node in diff_root.nodes_by_id.values():
505
+ for node in diff_root.nodes_by_identifier.values():
499
506
  for attribute_name in node.attributes_by_name:
500
- node_field_specifiers_map[node.uuid].add(attribute_name)
507
+ node_field_specifiers_map.add_entry(node_uuid=node.uuid, kind=node.kind, field_name=attribute_name)
501
508
  for relationship_diff in node.relationships_by_identifier.values():
502
- node_field_specifiers_map[node.uuid].add(relationship_diff.identifier)
509
+ node_field_specifiers_map.add_entry(
510
+ node_uuid=node.uuid, kind=node.kind, field_name=relationship_diff.identifier
511
+ )
503
512
  return node_field_specifiers_map
504
513
 
505
- def _remove_node_specifiers(
506
- self, node_specifiers: dict[str, set[str]], node_specifiers_to_remove: dict[str, set[str]]
507
- ) -> dict[str, set[str]]:
508
- final_node_specifiers: dict[str, set[str]] = defaultdict(set)
509
- for node_uuid, field_names_set in node_specifiers.items():
510
- specifiers_to_remove = node_specifiers_to_remove.get(node_uuid, set())
511
- final_specifiers = field_names_set - specifiers_to_remove
512
- if final_specifiers:
513
- final_node_specifiers[node_uuid] = final_specifiers
514
- return final_node_specifiers
515
-
516
- def get_new_node_field_specifiers(self) -> dict[str, set[str]]:
514
+ def get_new_node_field_specifiers(self) -> NodeFieldSpecifierMap:
517
515
  if self._new_node_field_specifiers is not None:
518
516
  return self._new_node_field_specifiers
519
517
  branch_node_specifiers = self.get_diff_node_field_specifiers()
520
- new_node_field_specifiers = self._remove_node_specifiers(
521
- branch_node_specifiers, self._previous_node_field_specifiers
522
- )
523
- self._new_node_field_specifiers = new_node_field_specifiers
524
- return new_node_field_specifiers
518
+ self._new_node_field_specifiers = branch_node_specifiers - self._previous_node_field_specifiers
519
+ return self._new_node_field_specifiers
525
520
 
526
- def get_current_node_field_specifiers(self) -> dict[str, set[str]]:
521
+ def get_current_node_field_specifiers(self) -> NodeFieldSpecifierMap:
527
522
  if self._current_node_field_specifiers is not None:
528
523
  return self._current_node_field_specifiers
529
524
  new_node_field_specifiers = self.get_new_node_field_specifiers()
530
- current_node_field_specifiers = self._remove_node_specifiers(
531
- self._previous_node_field_specifiers, new_node_field_specifiers
532
- )
533
- self._current_node_field_specifiers = current_node_field_specifiers
534
- return current_node_field_specifiers
525
+ self._current_node_field_specifiers = self._previous_node_field_specifiers - new_node_field_specifiers
526
+ return self._current_node_field_specifiers
535
527
 
536
528
  def read_result(self, query_result: QueryResult) -> None:
537
529
  path = query_result.get_path(label="diff_path")
@@ -565,11 +557,12 @@ class DiffQueryParser:
565
557
  return self._diff_root_by_branch[branch]
566
558
 
567
559
  def _get_diff_node(self, database_path: DatabasePath, diff_root: DiffRootIntermediate) -> DiffNodeIntermediate:
568
- node_id = database_path.node_id
569
- if node_id not in diff_root.nodes_by_id:
570
- diff_root.nodes_by_id[node_id] = DiffNodeIntermediate(
571
- uuid=node_id,
572
- kind=database_path.node_kind,
560
+ identifier = NodeIdentifier(
561
+ uuid=database_path.node_id, kind=database_path.node_kind, labels=database_path.node_labels
562
+ )
563
+ if identifier not in diff_root.nodes_by_identifier:
564
+ diff_root.nodes_by_identifier[identifier] = DiffNodeIntermediate(
565
+ identifier=identifier,
573
566
  db_id=database_path.node_db_id,
574
567
  from_time=database_path.node_changed_at,
575
568
  status=database_path.node_status,
@@ -577,7 +570,7 @@ class DiffQueryParser:
577
570
  if database_path.node_branch_support is BranchSupportType.AGNOSTIC
578
571
  else None,
579
572
  )
580
- diff_node = diff_root.nodes_by_id[node_id]
573
+ diff_node = diff_root.nodes_by_identifier[identifier]
581
574
  # special handling for nodes that have their kind updated, which results in 2 nodes with the same uuid
582
575
  if diff_node.db_id != database_path.node_db_id and (
583
576
  database_path.node_changed_at > diff_node.from_time
@@ -587,7 +580,7 @@ class DiffQueryParser:
587
580
  == (RelationshipStatus.DELETED, RelationshipStatus.ACTIVE)
588
581
  )
589
582
  ):
590
- diff_node.kind = database_path.node_kind
583
+ diff_node.identifier.kind = database_path.node_kind
591
584
  diff_node.db_id = database_path.node_db_id
592
585
  diff_node.from_time = database_path.node_changed_at
593
586
  diff_node.status = database_path.node_status
@@ -634,7 +627,9 @@ class DiffQueryParser:
634
627
  from_time = self.from_time
635
628
  if branch_name == self.base_branch_name:
636
629
  new_node_field_specifiers = self.get_new_node_field_specifiers()
637
- if attribute_name in new_node_field_specifiers.get(diff_node.uuid, set()):
630
+ if new_node_field_specifiers.has_entry(
631
+ node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=attribute_name
632
+ ):
638
633
  from_time = self.diff_branched_from_time
639
634
  if attribute_name not in diff_node.attributes_by_name:
640
635
  diff_node.attributes_by_name[attribute_name] = DiffAttributeIntermediate(
@@ -678,7 +673,9 @@ class DiffQueryParser:
678
673
  from_time = self.from_time
679
674
  if branch_name == self.base_branch_name:
680
675
  new_node_field_specifiers = self.get_new_node_field_specifiers()
681
- if relationship_schema.get_identifier() in new_node_field_specifiers.get(diff_node.uuid, set()):
676
+ if new_node_field_specifiers.has_entry(
677
+ node_uuid=diff_node.uuid, kind=diff_node.kind, field_name=relationship_schema.get_identifier()
678
+ ):
682
679
  from_time = self.diff_branched_from_time
683
680
  diff_relationship = DiffRelationshipIntermediate(
684
681
  name=relationship_schema.name,
@@ -700,8 +697,8 @@ class DiffQueryParser:
700
697
  branch_diff_root = self._diff_root_by_branch.get(branch)
701
698
  if not branch_diff_root:
702
699
  continue
703
- for node_id, diff_node in branch_diff_root.nodes_by_id.items():
704
- base_diff_node = base_diff_root.nodes_by_id.get(node_id)
700
+ for identifier, diff_node in branch_diff_root.nodes_by_identifier.items():
701
+ base_diff_node = base_diff_root.nodes_by_identifier.get(identifier)
705
702
  if not base_diff_node:
706
703
  continue
707
704
  self._apply_attribute_previous_values(diff_node=diff_node, base_diff_node=base_diff_node)
@@ -771,7 +768,7 @@ class DiffQueryParser:
771
768
  base_diff_root = self._diff_root_by_branch.get(self.base_branch_name)
772
769
  if not base_diff_root:
773
770
  return
774
- for node_diff in base_diff_root.nodes_by_id.values():
771
+ for node_diff in base_diff_root.nodes_by_identifier.values():
775
772
  for attribute_diff in node_diff.attributes_by_name.values():
776
773
  for property_diff in attribute_diff.properties_by_type.values():
777
774
  ordered_diff_values = property_diff.get_ordered_values_asc()
@@ -1,3 +1,5 @@
1
+ import json
2
+
1
3
  from neo4j.graph import Node as Neo4jNode
2
4
  from neo4j.graph import Path as Neo4jPath
3
5
 
@@ -16,6 +18,7 @@ from ..model.path import (
16
18
  EnrichedDiffRoot,
17
19
  EnrichedDiffRootMetadata,
18
20
  EnrichedDiffSingleRelationship,
21
+ NodeIdentifier,
19
22
  deserialize_tracking_id,
20
23
  )
21
24
  from ..parent_node_adder import DiffParentNodeAdder, ParentNodeAddRequest
@@ -25,13 +28,15 @@ class EnrichedDiffDeserializer:
25
28
  def __init__(self, parent_adder: DiffParentNodeAdder) -> None:
26
29
  self.parent_adder = parent_adder
27
30
  self._diff_root_map: dict[str, EnrichedDiffRoot] = {}
28
- self._diff_node_map: dict[tuple[str, str], EnrichedDiffNode] = {}
29
- self._diff_node_attr_map: dict[tuple[str, str, str], EnrichedDiffAttribute] = {}
30
- self._diff_node_rel_group_map: dict[tuple[str, str, str], EnrichedDiffRelationship] = {}
31
- self._diff_node_rel_element_map: dict[tuple[str, str, str, str], EnrichedDiffSingleRelationship] = {}
32
- self._diff_prop_map: dict[tuple[str, str, str, str] | tuple[str, str, str, str, str], EnrichedDiffProperty] = {}
33
- # {EnrichedDiffRoot: [(node_uuid, parents_path: Neo4jPath), ...]}
34
- self._parents_path_map: dict[EnrichedDiffRoot, list[tuple[str, Neo4jPath]]] = {}
31
+ self._diff_node_map: dict[tuple[str, NodeIdentifier], EnrichedDiffNode] = {}
32
+ self._diff_node_attr_map: dict[tuple[str, NodeIdentifier, str], EnrichedDiffAttribute] = {}
33
+ self._diff_node_rel_group_map: dict[tuple[str, NodeIdentifier, str], EnrichedDiffRelationship] = {}
34
+ self._diff_node_rel_element_map: dict[tuple[str, NodeIdentifier, str, str], EnrichedDiffSingleRelationship] = {}
35
+ self._diff_prop_map: dict[
36
+ tuple[str, NodeIdentifier, str, str] | tuple[str, str, str, str, str], EnrichedDiffProperty
37
+ ] = {}
38
+ # {EnrichedDiffRoot: [(NodeIdentifier, parents_path: Neo4jPath), ...]}
39
+ self._parents_path_map: dict[EnrichedDiffRoot, list[tuple[NodeIdentifier, Neo4jPath]]] = {}
35
40
 
36
41
  def initialize(self) -> None:
37
42
  self._diff_root_map = {}
@@ -42,10 +47,12 @@ class EnrichedDiffDeserializer:
42
47
  self._diff_prop_map = {}
43
48
  self._parents_path_map = {}
44
49
 
45
- def _track_parents_path(self, enriched_root: EnrichedDiffRoot, node_uuid: str, parents_path: Neo4jPath) -> None:
50
+ def _track_parents_path(
51
+ self, enriched_root: EnrichedDiffRoot, node_identifier: NodeIdentifier, parents_path: Neo4jPath
52
+ ) -> None:
46
53
  if enriched_root not in self._parents_path_map:
47
54
  self._parents_path_map[enriched_root] = []
48
- self._parents_path_map[enriched_root].append((node_uuid, parents_path))
55
+ self._parents_path_map[enriched_root].append((node_identifier, parents_path))
49
56
 
50
57
  async def read_result(self, result: QueryResult, include_parents: bool) -> None:
51
58
  enriched_root = self._deserialize_diff_root(root_node=result.get_node("diff_root"))
@@ -58,7 +65,7 @@ class EnrichedDiffDeserializer:
58
65
  parents_path = result.get("parents_path")
59
66
  if parents_path and isinstance(parents_path, Neo4jPath):
60
67
  self._track_parents_path(
61
- enriched_root=enriched_root, node_uuid=enriched_node.uuid, parents_path=parents_path
68
+ enriched_root=enriched_root, node_identifier=enriched_node.identifier, parents_path=parents_path
62
69
  )
63
70
 
64
71
  node_conflict_node = result.get(label="diff_node_conflict")
@@ -130,17 +137,21 @@ class EnrichedDiffDeserializer:
130
137
  def _deserialize_parents(self) -> None:
131
138
  for enriched_root, node_path_tuples in self._parents_path_map.items():
132
139
  self.parent_adder.initialize(enriched_diff_root=enriched_root)
133
- for node_uuid, parents_path in node_path_tuples:
140
+ for node_identifier, parents_path in node_path_tuples:
134
141
  # Remove the node itself from the path
135
142
  parents_path_slice = parents_path.nodes[1:]
136
143
 
137
144
  # TODO Ensure the list is even
138
- current_node_uuid = node_uuid
145
+ current_node_identifier = node_identifier
139
146
  for rel, parent in zip(parents_path_slice[::2], parents_path_slice[1::2], strict=False):
147
+ parent_identifier = NodeIdentifier(
148
+ uuid=parent.get("uuid"),
149
+ kind=parent.get("kind"),
150
+ labels=frozenset(json.loads(parent.get("db_labels"))),
151
+ )
140
152
  parent_request = ParentNodeAddRequest(
141
- node_id=current_node_uuid,
142
- parent_id=parent.get("uuid"),
143
- parent_kind=parent.get("kind"),
153
+ node_identifier=current_node_identifier,
154
+ parent_identifier=parent_identifier,
144
155
  parent_label=parent.get("label"),
145
156
  parent_rel_name=rel.get("name"),
146
157
  parent_rel_identifier=rel.get("identifier"),
@@ -148,7 +159,7 @@ class EnrichedDiffDeserializer:
148
159
  parent_rel_label=rel.get("label"),
149
160
  )
150
161
  self.parent_adder.add_parent(parent_request=parent_request)
151
- current_node_uuid = parent.get("uuid")
162
+ current_node_identifier = parent_identifier
152
163
 
153
164
  @classmethod
154
165
  def _get_str_or_none_property_value(cls, node: Neo4jNode, property_name: str) -> str | None:
@@ -189,14 +200,16 @@ class EnrichedDiffDeserializer:
189
200
 
190
201
  def _deserialize_diff_node(self, node_node: Neo4jNode, enriched_root: EnrichedDiffRoot) -> EnrichedDiffNode:
191
202
  node_uuid = str(node_node.get("uuid"))
192
- node_key = (enriched_root.uuid, node_uuid)
203
+ node_kind = str(node_node.get("kind"))
204
+ node_db_labels = frozenset(json.loads(node_node.get("db_labels")))
205
+ node_identifier = NodeIdentifier(uuid=node_uuid, kind=node_kind, labels=node_db_labels)
206
+ node_key = (enriched_root.uuid, node_identifier)
193
207
  if node_key in self._diff_node_map:
194
208
  return self._diff_node_map[node_key]
195
209
 
196
210
  timestamp_str = self._get_str_or_none_property_value(node=node_node, property_name="changed_at")
197
211
  enriched_node = EnrichedDiffNode(
198
- uuid=node_uuid,
199
- kind=str(node_node.get("kind")),
212
+ identifier=node_identifier,
200
213
  label=str(node_node.get("label")),
201
214
  changed_at=Timestamp(timestamp_str) if timestamp_str else None,
202
215
  action=DiffAction(str(node_node.get("action"))),
@@ -215,7 +228,7 @@ class EnrichedDiffDeserializer:
215
228
  self, diff_attr_node: Neo4jNode, enriched_root: EnrichedDiffRoot, enriched_node: EnrichedDiffNode
216
229
  ) -> EnrichedDiffAttribute:
217
230
  attr_name = str(diff_attr_node.get("name"))
218
- attr_key = (enriched_root.uuid, enriched_node.uuid, attr_name)
231
+ attr_key = (enriched_root.uuid, enriched_node.identifier, attr_name)
219
232
  if attr_key in self._diff_node_attr_map:
220
233
  return self._diff_node_attr_map[attr_key]
221
234
 
@@ -238,7 +251,7 @@ class EnrichedDiffDeserializer:
238
251
  self, relationship_group_node: Neo4jNode, enriched_root: EnrichedDiffRoot, enriched_node: EnrichedDiffNode
239
252
  ) -> EnrichedDiffRelationship:
240
253
  diff_rel_name = str(relationship_group_node.get("name"))
241
- rel_key = (enriched_root.uuid, enriched_node.uuid, diff_rel_name)
254
+ rel_key = (enriched_root.uuid, enriched_node.identifier, diff_rel_name)
242
255
  if rel_key in self._diff_node_rel_group_map:
243
256
  return self._diff_node_rel_group_map[rel_key]
244
257
 
@@ -272,7 +285,7 @@ class EnrichedDiffDeserializer:
272
285
  diff_element_peer_id = str(relationship_element_node.get("peer_id"))
273
286
  rel_element_key = (
274
287
  enriched_root.uuid,
275
- enriched_node.uuid,
288
+ enriched_node.identifier,
276
289
  enriched_relationship_group.name,
277
290
  diff_element_peer_id,
278
291
  )
@@ -320,7 +333,7 @@ class EnrichedDiffDeserializer:
320
333
  enriched_root: EnrichedDiffRoot,
321
334
  ) -> EnrichedDiffProperty:
322
335
  diff_prop_type = str(diff_attr_property_node.get("property_type"))
323
- attr_property_key = (enriched_root.uuid, enriched_node.uuid, enriched_attr.name, diff_prop_type)
336
+ attr_property_key = (enriched_root.uuid, enriched_node.identifier, enriched_attr.name, diff_prop_type)
324
337
  if attr_property_key in self._diff_prop_map:
325
338
  return self._diff_prop_map[attr_property_key]
326
339
 
@@ -1,10 +1,10 @@
1
- from collections import defaultdict
2
1
  from typing import AsyncGenerator, Generator, Iterable
3
2
 
4
3
  from neo4j.exceptions import TransientError
5
4
 
6
5
  from infrahub import config
7
6
  from infrahub.core import registry
7
+ from infrahub.core.diff.query.drop_nodes import EnrichedDiffDropNodesQuery
8
8
  from infrahub.core.diff.query.field_summary import EnrichedDiffNodeFieldSummaryQuery
9
9
  from infrahub.core.diff.query.summary_counts_enricher import (
10
10
  DiffFieldsSummaryCountsEnricherQuery,
@@ -16,6 +16,7 @@ from infrahub.database import InfrahubDatabase, retry_db_transaction
16
16
  from infrahub.exceptions import ResourceNotFoundError
17
17
  from infrahub.log import get_logger
18
18
 
19
+ from ..model.field_specifiers_map import NodeFieldSpecifierMap
19
20
  from ..model.path import (
20
21
  ConflictSelection,
21
22
  EnrichedDiffConflict,
@@ -26,6 +27,7 @@ from ..model.path import (
26
27
  EnrichedDiffsMetadata,
27
28
  EnrichedNodeCreateRequest,
28
29
  NodeDiffFieldSummary,
30
+ NodeIdentifier,
29
31
  TimeRange,
30
32
  TrackingId,
31
33
  )
@@ -108,7 +110,7 @@ class DiffRepository:
108
110
  diff_branch_names: list[str],
109
111
  from_time: Timestamp | None = None,
110
112
  to_time: Timestamp | None = None,
111
- filters: dict | None = None,
113
+ filters: EnrichedDiffQueryFilters | None = None,
112
114
  include_parents: bool = True,
113
115
  limit: int | None = None,
114
116
  offset: int | None = None,
@@ -180,11 +182,11 @@ class DiffRepository:
180
182
  async def hydrate_diff_pair(
181
183
  self,
182
184
  enriched_diffs_metadata: EnrichedDiffsMetadata,
183
- node_uuids: Iterable[str] | None = None,
185
+ node_identifiers: Iterable[NodeIdentifier] | None = None,
184
186
  ) -> EnrichedDiffs:
185
- filters = None
186
- if node_uuids:
187
- filters = {"ids": list(node_uuids) if node_uuids is not None else None}
187
+ filters = EnrichedDiffQueryFilters()
188
+ if node_identifiers:
189
+ filters.identifiers = list(node_identifiers)
188
190
  hydrated_base_diff = await self.get_one(
189
191
  diff_branch_name=enriched_diffs_metadata.base_branch_name,
190
192
  diff_id=enriched_diffs_metadata.base_branch_diff.uuid,
@@ -207,7 +209,7 @@ class DiffRepository:
207
209
  diff_branch_name: str,
208
210
  tracking_id: TrackingId | None = None,
209
211
  diff_id: str | None = None,
210
- filters: dict | None = None,
212
+ filters: EnrichedDiffQueryFilters | None = None,
211
213
  include_parents: bool = True,
212
214
  ) -> EnrichedDiffRoot:
213
215
  enriched_diffs = await self.get(
@@ -274,6 +276,12 @@ class DiffRepository:
274
276
  )
275
277
  await single_node_query.execute(db=self.db)
276
278
 
279
+ async def _drop_nodes(self, diff_root: EnrichedDiffRoot, node_identifiers: list[NodeIdentifier]) -> None:
280
+ drop_node_query = await EnrichedDiffDropNodesQuery.init(
281
+ db=self.db, enriched_diff_uuid=diff_root.uuid, node_identifiers=node_identifiers
282
+ )
283
+ await drop_node_query.execute(db=self.db)
284
+
277
285
  @retry_db_transaction(name="enriched_diff_hierarchy_update")
278
286
  async def _run_hierarchy_links_update_query(self, diff_root_uuid: str, diff_nodes: list[EnrichedDiffNode]) -> None:
279
287
  log.info(f"Updating diff hierarchy links, num_nodes={len(diff_nodes)}")
@@ -321,7 +329,12 @@ class DiffRepository:
321
329
  node_uuids=node_uuids,
322
330
  )
323
331
 
324
- async def save(self, enriched_diffs: EnrichedDiffs | EnrichedDiffsMetadata, do_summary_counts: bool = True) -> None:
332
+ async def save(
333
+ self,
334
+ enriched_diffs: EnrichedDiffs | EnrichedDiffsMetadata,
335
+ do_summary_counts: bool = True,
336
+ node_identifiers_to_drop: list[NodeIdentifier] | None = None,
337
+ ) -> None:
325
338
  # metadata-only update
326
339
  if not isinstance(enriched_diffs, EnrichedDiffs):
327
340
  await self._save_root_metadata(enriched_diffs=enriched_diffs)
@@ -336,6 +349,8 @@ class DiffRepository:
336
349
  await self._save_node_batch(node_create_batch=node_create_batch)
337
350
  count_nodes_remaining -= len(node_create_batch)
338
351
  log.info(f"Batch saved. {count_nodes_remaining=}")
352
+ if node_identifiers_to_drop:
353
+ await self._drop_nodes(diff_root=enriched_diffs.diff_branch_diff, node_identifiers=node_identifiers_to_drop)
339
354
  await self._update_hierarchy_links(enriched_diffs=enriched_diffs)
340
355
  if do_summary_counts:
341
356
  await self._update_summary_counts(diff_root=enriched_diffs.diff_branch_diff)
@@ -501,21 +516,25 @@ class DiffRepository:
501
516
  await query.execute(db=self.db)
502
517
  return query.get_num_changes_by_branch()
503
518
 
504
- async def get_node_field_specifiers(self, diff_id: str) -> dict[str, set[str]]:
519
+ async def get_node_field_specifiers(self, diff_id: str) -> NodeFieldSpecifierMap:
505
520
  limit = config.SETTINGS.database.query_size_limit
506
521
  offset = 0
507
- specifiers: dict[str, set[str]] = defaultdict(set)
522
+ specifiers_map = NodeFieldSpecifierMap()
508
523
  while True:
509
524
  query = await EnrichedDiffFieldSpecifiersQuery.init(db=self.db, diff_id=diff_id, offset=offset, limit=limit)
510
525
  await query.execute(db=self.db)
511
526
  has_data = False
512
527
  for field_specifier_tuple in query.get_node_field_specifier_tuples():
513
- specifiers[field_specifier_tuple[0]].add(field_specifier_tuple[1])
528
+ specifiers_map.add_entry(
529
+ node_uuid=field_specifier_tuple[0],
530
+ kind=field_specifier_tuple[1],
531
+ field_name=field_specifier_tuple[2],
532
+ )
514
533
  has_data = True
515
534
  if not has_data:
516
535
  break
517
536
  offset += limit
518
- return specifiers
537
+ return specifiers_map
519
538
 
520
539
  async def add_summary_counts(
521
540
  self,
@@ -1 +1 @@
1
- GRAPH_VERSION = 26
1
+ GRAPH_VERSION = 27
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from infrahub.database.constants import IndexType
3
+ from infrahub.constants.database import IndexType
4
4
  from infrahub.database.index import IndexItem
5
5
 
6
6
  node_indexes: list[IndexItem] = [
@@ -17,6 +17,8 @@ node_indexes: list[IndexItem] = [
17
17
  IndexItem(name="diff_node_uuid", label="DiffNode", properties=["uuid"], type=IndexType.TEXT),
18
18
  ]
19
19
 
20
+ attr_value_index = IndexItem(name="attr_value", label="AttributeValue", properties=["value"], type=IndexType.RANGE)
21
+
20
22
  rel_indexes: list[IndexItem] = [
21
23
  IndexItem(name="attr_from", label="HAS_ATTRIBUTE", properties=["from"], type=IndexType.RANGE),
22
24
  IndexItem(name="attr_branch", label="HAS_ATTRIBUTE", properties=["branch"], type=IndexType.RANGE),
@@ -1,7 +1,9 @@
1
1
  import importlib
2
+ from typing import TYPE_CHECKING
2
3
  from uuid import uuid4
3
4
 
4
5
  from infrahub import config, lock
6
+ from infrahub.constants.database import DatabaseType
5
7
  from infrahub.core import registry
6
8
  from infrahub.core.branch import Branch
7
9
  from infrahub.core.constants import (
@@ -13,6 +15,7 @@ from infrahub.core.constants import (
13
15
  PermissionDecision,
14
16
  )
15
17
  from infrahub.core.graph import GRAPH_VERSION
18
+ from infrahub.core.graph.index import attr_value_index, node_indexes, rel_indexes
16
19
  from infrahub.core.manager import NodeManager
17
20
  from infrahub.core.node import Node
18
21
  from infrahub.core.node.ipam import BuiltinIPPrefix
@@ -25,6 +28,8 @@ from infrahub.core.root import Root
25
28
  from infrahub.core.schema import SchemaRoot, core_models, internal_schema
26
29
  from infrahub.core.schema.manager import SchemaManager
27
30
  from infrahub.database import InfrahubDatabase
31
+ from infrahub.database.memgraph import IndexManagerMemgraph
32
+ from infrahub.database.neo4j import IndexManagerNeo4j
28
33
  from infrahub.exceptions import DatabaseError
29
34
  from infrahub.graphql.manager import GraphQLSchemaManager
30
35
  from infrahub.log import get_logger
@@ -32,6 +37,9 @@ from infrahub.menu.utils import create_default_menu
32
37
  from infrahub.permissions import PermissionBackend
33
38
  from infrahub.storage import InfrahubObjectStorage
34
39
 
40
+ if TYPE_CHECKING:
41
+ from infrahub.database.index import IndexManagerBase
42
+
35
43
  log = get_logger()
36
44
 
37
45
 
@@ -115,7 +123,19 @@ async def initialize_registry(db: InfrahubDatabase, initialize: bool = False) ->
115
123
  registry.permission_backends = initialize_permission_backends()
116
124
 
117
125
 
118
- async def initialization(db: InfrahubDatabase) -> None:
126
+ async def add_indexes(db: InfrahubDatabase) -> None:
127
+ if db.db_type is DatabaseType.MEMGRAPH:
128
+ index_manager: IndexManagerBase = IndexManagerMemgraph(db=db)
129
+ index_manager = IndexManagerNeo4j(db=db)
130
+
131
+ if config.SETTINGS.experimental_features.value_db_index:
132
+ node_indexes.append(attr_value_index)
133
+ index_manager.init(nodes=node_indexes, rels=rel_indexes)
134
+ log.debug("Loading database indexes ..")
135
+ await index_manager.add()
136
+
137
+
138
+ async def initialization(db: InfrahubDatabase, add_database_indexes: bool = False) -> None:
119
139
  if config.SETTINGS.database.db_type == config.DatabaseType.MEMGRAPH:
120
140
  session = await db.session()
121
141
  await session.run(query="SET DATABASE SETTING 'log.level' TO 'INFO'")
@@ -129,12 +149,8 @@ async def initialization(db: InfrahubDatabase) -> None:
129
149
  log.debug("Checking Root Node")
130
150
  await initialize_registry(db=db, initialize=True)
131
151
 
132
- # Add Indexes to the database
133
- if db.manager.index.initialized:
134
- log.debug("Loading database indexes ..")
135
- await db.manager.index.add()
136
- else:
137
- log.warning("The database index manager hasn't been initialized.")
152
+ if add_database_indexes:
153
+ await add_indexes(db=db)
138
154
 
139
155
  # ---------------------------------------------------
140
156
  # Load all schema in the database into the registry