Skip to content

Commit 7e84745

Browse files
authored
skip running node-level uniqueness constraints when possible (#5954)
* skip running node-level uniqueness constraints when possible * fix unit test for new order of constraint execution * add changelog * add schema's to vale
1 parent 4ce87be commit 7e84745

File tree

5 files changed

+107
-9
lines changed

5 files changed

+107
-9
lines changed

.vale/styles/spelling-exceptions.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ regenerate_host_artifact
109109
repo
110110
REST
111111
resources
112+
schema's
112113
schema_mapping
113114
sdk
114115
subcommand

backend/infrahub/core/node/constraints/grouped_uniqueness.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,25 @@ async def _check_one_schema(
177177
await self._check_results(updated_node=node, path_groups=path_groups, query_results=query.get_results())
178178

179179
async def check(self, node: Node, at: Optional[Timestamp] = None, filters: Optional[list[str]] = None) -> None:
180+
def _frozen_constraints(schema: MainSchemaTypes) -> frozenset[frozenset[str]]:
181+
if not schema.uniqueness_constraints:
182+
return frozenset()
183+
return frozenset(frozenset(uc) for uc in schema.uniqueness_constraints)
184+
180185
node_schema = node.get_schema()
181-
schemas_to_check: list[MainSchemaTypes] = [node_schema]
186+
include_node_schema = True
187+
frozen_node_constraints = _frozen_constraints(node_schema)
188+
schemas_to_check: list[MainSchemaTypes] = []
182189
if node_schema.inherit_from:
183190
for parent_schema_name in node_schema.inherit_from:
184191
parent_schema = self.schema_branch.get(name=parent_schema_name, duplicate=False)
185-
if parent_schema.uniqueness_constraints:
186-
schemas_to_check.append(parent_schema)
192+
if not parent_schema.uniqueness_constraints:
193+
continue
194+
schemas_to_check.append(parent_schema)
195+
frozen_parent_constraints = _frozen_constraints(parent_schema)
196+
if frozen_node_constraints <= frozen_parent_constraints:
197+
include_node_schema = False
198+
if include_node_schema:
199+
schemas_to_check.append(node_schema)
187200
for schema in schemas_to_check:
188201
await self._check_one_schema(node=node, node_schema=schema, at=at, filters=filters)

backend/tests/unit/core/constraint_validators/test_node_grouped_uniqueness.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from unittest.mock import patch
2+
13
import pytest
24

35
from infrahub.core import registry
46
from infrahub.core.branch import Branch
57
from infrahub.core.node import Node
68
from infrahub.core.node.constraints.grouped_uniqueness import NodeGroupedUniquenessConstraint
9+
from infrahub.core.validators.uniqueness.query import NodeUniqueAttributeConstraintQuery
710
from infrahub.database import InfrahubDatabase
811
from infrahub.exceptions import ValidationError
912

@@ -197,6 +200,86 @@ async def test_uniqueness_constraint_no_conflict_relationship_and_attribute(
197200

198201
await self.__call_system_under_test(db=db, branch=default_branch, node=car_node)
199202

203+
@pytest.mark.parametrize(
204+
["node_constraints", "parent_constraints", "node_query_should_run"],
205+
[
206+
(
207+
[
208+
["nbr_seats__value", "name__value"],
209+
["previous_owner", "nbr_seats__value"],
210+
],
211+
[
212+
["nbr_seats__value", "name__value"],
213+
["previous_owner", "nbr_seats__value"],
214+
],
215+
False,
216+
),
217+
(
218+
[
219+
["nbr_seats__value", "name__value"],
220+
],
221+
[
222+
["nbr_seats__value", "name__value"],
223+
["previous_owner", "nbr_seats__value"],
224+
],
225+
False,
226+
),
227+
(
228+
[
229+
["previous_owner", "name__value"],
230+
],
231+
[
232+
["nbr_seats__value", "name__value"],
233+
["previous_owner", "nbr_seats__value"],
234+
],
235+
True,
236+
),
237+
(
238+
[
239+
["nbr_seats__value", "name__value"],
240+
["previous_owner", "nbr_seats__value", "color__value"],
241+
],
242+
[
243+
["nbr_seats__value", "name__value"],
244+
["previous_owner", "nbr_seats__value"],
245+
],
246+
True,
247+
),
248+
],
249+
)
250+
async def test_uniqueness_constraint_skips_overlapping_constraints(
251+
self,
252+
db: InfrahubDatabase,
253+
default_branch: Branch,
254+
car_person_generics_data_simple,
255+
node_constraints: list[list[str]],
256+
parent_constraints: list[list[str]],
257+
node_query_should_run: bool,
258+
):
259+
car_node: Node = car_person_generics_data_simple["c1"]
260+
car_schema = car_node.get_schema()
261+
schema_branch = registry.schema.get_schema_branch(name=default_branch.name)
262+
parent_kind = car_schema.inherit_from[0]
263+
parent_schema = schema_branch.get(name=parent_kind, duplicate=False)
264+
car_schema.uniqueness_constraints = node_constraints
265+
parent_schema.uniqueness_constraints = parent_constraints
266+
267+
# make sure we only use this query once if the constraints overlap
268+
with patch(
269+
"infrahub.core.node.constraints.grouped_uniqueness.NodeUniqueAttributeConstraintQuery",
270+
wraps=NodeUniqueAttributeConstraintQuery,
271+
) as wrapped_query:
272+
await self.__call_system_under_test(db=db, branch=default_branch, node=car_node)
273+
if node_query_should_run:
274+
assert len(wrapped_query.init.call_args_list) == 2
275+
else:
276+
assert len(wrapped_query.init.call_args_list) == 1
277+
query_kinds = {init_call[1]["query_request"].kind for init_call in wrapped_query.init.call_args_list}
278+
if node_query_should_run:
279+
assert query_kinds == {car_schema.kind, parent_kind}
280+
else:
281+
assert query_kinds == {parent_kind}
282+
200283
async def test_uniqueness_constraint_conflict_relationship_and_attribute(
201284
self, db: InfrahubDatabase, default_branch: Branch, car_person_generics_data_simple
202285
):

backend/tests/unit/core/constraint_validators/test_node_uniqueness.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ async def test_hierarchical_uniqueness_constraint(
4242
site_schema.uniqueness_constraints = [["parent", "name__value"]]
4343

4444
rack_schema = hierarchical_location_schema_simple_unregistered.get(name="LocationRack")
45-
rack_schema.human_friendly_id = ["parent__name__value", "name__value"]
46-
rack_schema.uniqueness_constraints = [["parent", "name__value"]]
45+
rack_schema.human_friendly_id = ["parent__name__value", "status__value"]
46+
rack_schema.uniqueness_constraints = [["parent", "status__value"]]
4747

4848
registry.schema.register_schema(schema=hierarchical_location_schema_simple_unregistered, branch=default_branch.name)
4949
constraint = NodeGroupedUniquenessConstraint(db=db, branch=default_branch)
@@ -67,7 +67,7 @@ async def test_hierarchical_uniqueness_constraint(
6767
await ld6.save(db=db)
6868
await constraint.check(ld6)
6969

70-
ld6 = await Node.init(db=db, schema="LocationRack", branch=default_branch)
71-
await ld6.new(db=db, name="ld6-ldn", parent=uk)
72-
with pytest.raises(ValidationError, match=r"Violates uniqueness constraint 'parent-name' at parent"):
73-
await constraint.check(ld6)
70+
ld62 = await Node.init(db=db, schema="LocationRack", branch=default_branch)
71+
await ld62.new(db=db, name="ld6-ldn2", parent=uk)
72+
with pytest.raises(ValidationError, match=r"Violates uniqueness constraint 'parent-status' at status"):
73+
await constraint.check(ld62)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reduces the number of database queries we run when checking a uniqueness constraint during a node update or create mutation if that node uses a schema that inherits from a generic schema and the node schema's uniqueness constraints are contained within the generic schema's uniqueness constraints

0 commit comments

Comments
 (0)