Skip to content

bug: Removing an attribute referenced by a stale child uniqueness_constraint returns HTTP 500 instead of a clean validation error #8990

Description

@PhillSimonds

Component

No response

Infrahub version

1.8.5

Current Behavior

If a node inherits identity fields (human_friendly_id, uniqueness_constraints) from a generic, those fields are baked into the inheritor at creation time and do not cascade on later generic changes. If the generic's identity fields are subsequently changed to reference a different attribute, inheritors are left with stale constraints still pointing at the original attribute.

Attempting to remove that original attribute from the generic via state: absent produces HTTP 500 Internal Server Error. The server-side traceback:

File "/source/backend/infrahub/core/schema/schema_branch.py", line 856, in sync_uniqueness_constraints_and_unique_attributes
    raise ValueError(
        f"{node_schema.kind}: Requested unique constraint not found within node. (`{constraint_path}`)"
    ) from exc
ValueError: <ChildNode>: Requested unique constraint not found within node. (`<old_attr>__value`)

This is raised in process_pre_validation during sync_uniqueness_constraints_and_unique_attributes, after the schema-load pipeline has already moved past the pre-write validator.

The delete commits to Neo4j despite the 500 — the same non-atomic write pattern as found in #8988. The attribute is removed from the generic, but the inheriting node still carries the stale uniqueness_constraints entry referencing the now-deleted attribute name. This leaves the branch in a state where every subsequent /api/schema/load call fails with the same Requested unique constraint not found within node error, because the post-write validator re-runs on every load and the orphaned constraint is still there. The branch cannot be unwedged through the public API until the orphaned inheritor is removed or its uniqueness_constraints fixed — and neither can be done because the schema load that would fix them is blocked by the same error.

Expected Behavior

Modifying human_friendly_id or uniqueness_constraints on a generic would ideally propagate to inheriting nodes that haven't explicitly overridden those fields, rather than requiring each inheritor to be updated individually to stay consistent with its generic. In the current design these values are baked into each inheritor at creation time and don't cascade on later generic changes, which creates the inconsistent-state path this bug report describes.

Separately, when an operation does leave the graph inconsistent, it would be preferable for the write to roll back rather than partially commit.

Steps to Reproduce

Three YAML files, three infrahubctl schema load calls on a fresh Infrahub 1.8.5 instance.

1. Load baseline — baseline.yml

A generic with two attributes (old_id / new_id), using old_id for identity. One child inherits.

---
version: "1.0"

generics:
  - name: Parent
    namespace: Bug5b
    human_friendly_id: ["old_id__value"]
    display_label: "{{ old_id__value }}"
    order_by: ["old_id__value"]
    include_in_menu: false
    attributes:
      - name: old_id
        kind: Number
        optional: true
      - name: new_id
        kind: NumberPool
        read_only: true
        branch: agnostic

nodes:
  - name: Child
    namespace: Bug5b
    inherit_from:
      - Bug5bParent
    include_in_menu: false
infrahubctl schema load baseline.yml   # succeeds

Verify: both Bug5bParent and Bug5bChild have human_friendly_id: ['old_id__value'] and uniqueness_constraints: [['old_id__value']].

2. Switch generic identity to new_idswitch-generic.yml

---
version: "1.0"
generics:
  - name: Parent
    namespace: Bug5b
    human_friendly_id: ["new_id__value"]
    display_label: "{{ new_id__value }}"
    order_by: ["new_id__value"]
infrahubctl schema load switch-generic.yml   # succeeds

Verify: Bug5bParent.hfid is now ['new_id__value'], but Bug5bChild.hfid and uniqueness_constraints still point at old_id__value. This is expected — identity fields don't cascade — and it is the inconsistent state that triggers the bug.

3. Remove old_idremove-old.yml

---
version: "1.0"
generics:
  - name: Parent
    namespace: Bug5b
    attributes:
      - name: old_id
        kind: Number
        state: absent
infrahubctl schema load remove-old.yml

Returns HTTP 500:

HTTP communication failure: Server error '500 Internal Server Error' for url
'http://.../api/schema/load?branch=main'

Server log shows:

ValueError: Bug5bChild: Requested unique constraint not found within node. (`old_id__value`)
  File ".../schema/schema_branch.py", line 856, in sync_uniqueness_constraints_and_unique_attributes

4. Verify persistent corruption

curl -s "$INFRAHUB_ADDRESS/api/schema?branch=main" | python3 -c "
import sys,json
d=json.load(sys.stdin)
g=[n for n in d['generics'] if n['kind']=='Bug5bParent'][0]
c=[n for n in d['nodes'] if n['kind']=='Bug5bChild'][0]
print('Bug5bParent.attrs:', [(a['name'],a['kind']) for a in g['attributes'] if not a['name'].startswith('_')])
print('Bug5bChild.uniq: ', c['uniqueness_constraints'])
"

Expected to show the corrupted state after step 3:

Bug5bParent.attrs: [('new_id', 'NumberPool')]             ← old_id is gone
Bug5bChild.uniq:   [['old_id__value']]                    ← still references deleted attribute

Any attempt to load any schema on the branch after this point — including a no-op load, or a fix-attempt for Bug5bChild — fails with the same error: Bug5bChild: Requested unique constraint not found within node. (old_id__value). The branch is unrecoverable through the schema-load API and requires database intervention or a redeploy, same as bug-report-3.

Additional Information

Related

Proposed fix direction

In order of desirability:

  1. Propagate generic identity-field changes to inheritors (human_friendly_id, uniqueness_constraints, display_label, order_by). Respect explicit overrides at the inheritor level — i.e. only cascade where the inheritor's value was inherited, not explicitly set. This eliminates the "stale baked-in identity" class of problem entirely and removes the need for users to manually chase down every inheritor during a migration.
  2. If full propagation isn't adopted, extend the pre-write validator to detect the inconsistent state and reject cleanly with a 4xx error that enumerates the inheritors needing manual update. The error should tell the user what to do, not just block them.
  3. Independently of either, wrap the Neo4j write + schema.process() call inside update_schema_branch in a single transaction so validation failures in post-processing roll back cleanly. This also remediates the non-deterministic partial-commit behavior observed during the multi-inheritor case. This is the same fix called for in bug: Schema delete of a generic with live inherit_from references is accepted, partially commits to Neo4j, crashes in process_inheritance, and permanently wedges the branch. #8988.

Prevention (for users doing this migration today)

Before removing an attribute, explicitly update every inheritor's human_friendly_id, uniqueness_constraints, display_label, and order_by to stop referencing that attribute. Identity fields do not cascade from the generic — each inheritor must be updated by name, with inherit_from restated explicitly in the update YAML, or an inherit_from constraint violation will block the update. Only once every inheritor is verified clean should the attribute-removal apply be attempted.

Once the 500 has fired the branch is effectively unrecoverable (same as #8988) — restore from backup or redeploy.

Environment

  • Infrahub: 1.8.5
  • Helm chart: opsmill/infrahub 4.21.0 (global.infrahubTag=1.8.5)
  • Cluster: KIND 0.31.0 on Kubernetes 1.35.0
  • Trigger: three sequential infrahubctl schema load calls on main in a fresh deploy

Metadata

Metadata

Labels

priority/2This issue stalls work on the project or its dependents, it's a blocker for a releasetype/bugSomething isn't working as expected

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions