Skip to content

IFC-1258: Fix relationship mutation constraints#9270

Open
solababs wants to merge 4 commits into
stablefrom
fix-relationship-mutation-constraints-ifc-1258
Open

IFC-1258: Fix relationship mutation constraints#9270
solababs wants to merge 4 commits into
stablefrom
fix-relationship-mutation-constraints-ifc-1258

Conversation

@solababs
Copy link
Copy Markdown
Contributor

@solababs solababs commented May 18, 2026

Why

RelationshipAdd and RelationshipRemove mutations did not enforce two schema constraints:

  1. RelationshipAdd allowed adding multiple peers to a cardinality-one relationship, silently violating the schema.
  2. RelationshipRemove allowed removing the last peer from an optional=False relationship, leaving the node in an invalid state.

Closes #5794

How to test

uv run pytest backend/tests/component/graphql/test_mutation_relationship.py -v

Impact & rollout

  • Backward compatibility: Callers that were relying on the ability to add a second peer to a cardinality-one relationship via RelationshipAdd, or to remove the last peer from an optional=False relationship via RelationshipRemove, will now receive a ValidationError. These operations were already schema violations — this PR makes the API consistent with the schema definition.
  • Performance: One additional get_peers() DB query per RelationshipAdd on cardinality-one relationships; one per RelationshipRemove on optional=False relationships. Negligible overhead.

Checklist

  • Tests added/updated
  • Changelog entry added (backend/changelog/5794.fixed.md)
  • External docs updated (if user-facing or ops-facing change)
  • Internal .md docs updated (internal knowledge and AI code tools knowledge)
  • I have reviewed AI generated content

Summary by cubic

Enforces schema constraints for GraphQL relationship mutations to fix IFC-1258. Supports cardinality-one relationships: the first add is allowed; violations return ValidationError.

  • Bug Fixes
    • RelationshipAdd rejects adding a second peer to a cardinality-one relationship.
    • RelationshipRemove rejects removing the last peer from an optional=False relationship; partial removals are allowed.

Written for commit 0895d2e. Summary will update on new commits. Review in cubic

@solababs solababs requested a review from a team as a code owner May 18, 2026 08:28
@github-actions github-actions Bot added the group/backend Issue related to the backend (API Server, Git Agent) label May 18, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Confidence score: 2/5

  • There is a high-confidence, high-severity logic gap in backend/infrahub/graphql/mutations/relationship.py: cardinality-one validation only checks pre-existing peers, so a single request can still create multiple new peers.
  • This can violate IFC-1258 and lead to concrete data-integrity/regression risk in relationship mutations, so this is not just a minor edge case.
  • Pay close attention to backend/infrahub/graphql/mutations/relationship.py - enforce cardinality checks against newly added peers within the same request.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/infrahub/graphql/mutations/relationship.py">

<violation number="1" location="backend/infrahub/graphql/mutations/relationship.py:498">
P1: Cardinality-one validation is incomplete: it checks only pre-existing peers, so one request can still add multiple new peers and violate IFC-1258.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

Comment on lines +498 to +500
if existing_peers:
raise ValidationError(
{"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Cardinality-one validation is incomplete: it checks only pre-existing peers, so one request can still add multiple new peers and violate IFC-1258.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/infrahub/graphql/mutations/relationship.py, line 498:

<comment>Cardinality-one validation is incomplete: it checks only pre-existing peers, so one request can still add multiple new peers and violate IFC-1258.</comment>

<file context>
@@ -492,6 +490,30 @@ async def _collect_current_peers(
+    rel_schema = source_node.get_schema().get_relationship(name=relationship_name)
+    if rel_schema.cardinality == RelationshipCardinality.ONE:
+        existing_peers = await _collect_current_peers(info=info, data=data, source_node=source_node)
+        if existing_peers:
+            raise ValidationError(
+                {"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"}
</file context>
Suggested change
if existing_peers:
raise ValidationError(
{"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"}
if existing_peers or len(data.get("nodes") or []) > 1:
raise ValidationError(
{"name": f"'{relationship_name}' is a cardinality-one relationship and already has a peer"}
)

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 18, 2026

Merging this PR will not alter performance

✅ 12 untouched benchmarks


Comparing fix-relationship-mutation-constraints-ifc-1258 (0895d2e) with stable (72c3033)

Open in CodSpeed

await _validate_permissions(info=info, source_node=source, peers=nodes)
await _validate_peer_types(info=info, data=data, source_node=source, peers=nodes)
await _validate_peer_parents(info=info, data=data, source_node=source, peers=nodes)
await _validate_cardinality_add(info=info, data=data, source_node=source)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like this could be called after the call to _collect_current_peers on line 101/102 and it could receive the rel_schema and existing_peers as arguments to avoid calling _collect_current_peers twice

@classmethod
@retry_db_transaction(name="relationship_add")
async def mutate(
async def mutate( # noqa: PLR0915
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to add this an an exception for this file in pyproject.toml somewhere so that it is included in our general list of "things to clean up?"

nodes = await _validate_peers(info=info, data=data)
await _validate_permissions(info=info, source_node=source, peers=nodes)
await _validate_peer_types(info=info, data=data, source_node=source, peers=nodes)
await _validate_optional_remove(info=info, data=data, source_node=source)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same idea here to avoid multiple calls to _collect_current_peers

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@solababs solababs requested a review from ajtmccarty May 19, 2026 10:03
Copy link
Copy Markdown
Contributor

@ajtmccarty ajtmccarty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the robot might have a point with its comment. now that the cardinality-many check is removed, it actually might be possible to try to add multiple peers to a cardinality-one relationship. this might require a new test

""" % (
person_jack_main.id,
tag_red_main.id,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we generally try to do this with graphql variables now


rel_schema = source.get_schema().get_relationship(name=relationship_name)
if rel_schema.cardinality != RelationshipCardinality.MANY:
raise ValidationError({"name": f"'{relationship_name}' must be a relationship of cardinality Many"})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes it look like the first bug this PR is fixing should have already been blocked

RelationshipAdd allowed adding multiple peers to a cardinality-one relationship, silently violating the schema.

assert source2_final.id == profile.id


async def test_relationship_add_cardinality_one_rejected(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like it is already tested above in test_relationship_wrong_name.
I'd say that this test should cover successfully adding a card-one relationship via graphql and then failing to add a second

assert primary_tag.id == tag_blue_main.id


async def test_relationship_remove_mandatory_rejected(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test and the one below look like they can be combined into a class to share fixtures and reduce loading time

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

@solababs solababs requested a review from ajtmccarty May 25, 2026 07:37
}) {
ok
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional. I think you can refactor this so that this query is defined in one place and uses graphql variables so that it does not have to be generated within the tests multiple times. it would make these tests easier to read

@@ -0,0 +1 @@
RelationshipAdd now rejects additions that would violate a cardinality-one constraint. RelationshipRemove now rejects removals that would leave a mandatory (optional=False) relationship without any peer.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the mutation already rejected cardinality-one constraint violations b/c it rejected any request to change a cardinality-one relationship. I think the fix is 1) that it now allows updating a cardinality-one relationship and 2) that it respects a mandatory constraint on a cardinality many relationship

variable_values={"id": person_jack_main.id, "node_id": tag_red_main.id},
)
assert result.errors
assert result.errors[0].message == "'primary_tag' is a cardinality-one relationship and already has a peer at name"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error looks wrong, specifically the at name piece at the end. does the error need to be raised in a different way to apply to the primary_tag field?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group/backend Issue related to the backend (API Server, Git Agent)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Mutation RelationshipAdd and RelationshipRemove allows removing parent/child causing node to break.

2 participants