Component
No response
Infrahub version
1.8.5
Current Behavior
Triggered by the following two-step sequence (each step a separate POST /api/schema/load — either two infrahubctl schema load calls or two commits via git integration):
- Step 1: apply a schema that sets
state: absent on every peer-kind relationship pointing at a generic. This clears all peer references to the generic. inherit_from references from other nodes are untouched. Apply succeeds.
- Step 2: apply a schema that sets
state: absent on the generic itself.
On step 2:
-
The pre-write validator checks peer references. After step 1 there are none, so validation passes. inherit_from references are not checked.
-
The delete is persisted to Neo4j.
-
Post-write, schema.process() → process_inheritance() at backend/infrahub/core/schema/schema_branch.py:1795 raises ValueError: <Node> Unable to find the generic <Generic> because the inheriting node still carries an inherit_from edge to the just-deleted generic.
-
The client receives HTTP 500 Internal Server Error.
-
The delete is not rolled back. The schema graph is now corrupted.
-
Every subsequent call to POST /api/schema/load on the branch — including re-declaring the generic with or without the original UUID — fails with:
Unable to find the Schema associated with <original-UUID>, <Generic>
The branch cannot be unwedged through the public API.
Note: if the same two mutations are applied in a single POST /api/schema/load (peer removal and generic deletion together in one file), the validator catches the inconsistency and rejects it cleanly with <Node>: Relationship '<rel>' is referring an invalid peer '<Generic>'. The bug only manifests when the steps are split across two separate applies — which is the normal case for git integration, where each commit with schema changes is its own apply.
Expected Behavior
On step 2 above, the delete validator should symmetrically check inherit_from references alongside peer references and reject the operation cleanly before any DB write, with a message like:
<InheritingNode>: Generic '<Generic>' cannot be deleted while referenced via inherit_from
The schema graph should remain unchanged.
Steps to Reproduce
A fresh Infrahub 1.8.5 instance with no prior data. Three YAML files, three infrahubctl schema load calls. A trivial three-kind schema is sufficient.
1. Load the baseline schema — baseline.yml
---
version: "1.0"
generics:
- name: Parent
namespace: Bugrepo
description: "Generic that will be deleted. Has inheritors."
human_friendly_id: ["rule_id__value"]
display_label: "{{ rule_id__value }}"
include_in_menu: false
order_by:
- rule_id__value
attributes:
- name: rule_id
kind: Number
optional: true
- name: Device
namespace: Bugrepo
description: "Generic holding a peer relationship to BugrepoParent."
human_friendly_id: ["name__value"]
display_label: "{{ name__value }}"
include_in_menu: false
attributes:
- name: name
kind: Text
relationships:
- name: rules
peer: BugrepoParent
kind: Generic
cardinality: many
optional: true
nodes:
- name: Child
namespace: Bugrepo
description: "Node that inherits from BugrepoParent. Will be orphaned."
inherit_from:
- BugrepoParent
include_in_menu: false
infrahubctl schema load baseline.yml # succeeds
2. Remove the peer reference from BugrepoDevice — step1.yml
---
version: "1.0"
generics:
- name: Device
namespace: Bugrepo
relationships:
- name: rules
peer: BugrepoParent
state: absent
infrahubctl schema load step1.yml # succeeds
After this, BugrepoParent has no peer: references. It still has one inherit_from: reference from BugrepoChild.
3. Delete the generic — step2.yml
---
version: "1.0"
generics:
- name: Parent
namespace: Bugrepo
state: absent
infrahubctl schema load step2.yml
Returns HTTP 500 Internal Server Error. Server traceback:
File "/source/backend/infrahub/api/schema.py", line 392, in load_schema
updated_hash = await coordinator.execute(...)
File "/source/backend/infrahub/core/schema/update_coordinator.py", line 167, in execute
updated_hash = await self._update_schema(...)
File "/source/backend/infrahub/core/schema/update_coordinator.py", line 236, in _update_schema
await self.schema_manager.update_schema_branch(...)
File "/source/backend/infrahub/core/schema/manager.py", line 217, in update_schema_branch
updated_schema = await self.load_schema_from_db(...)
File "/source/backend/infrahub/core/schema/manager.py", line 837, in load_schema_from_db
schema.process(validate_schema=validate_schema)
File "/source/backend/infrahub/core/schema/schema_branch.py", line 624, in process
self.process_pre_validation()
File "/source/backend/infrahub/core/schema/schema_branch.py", line 645, in process_pre_validation
self.process_inheritance()
File "/source/backend/infrahub/core/schema/schema_branch.py", line 1795, in process_inheritance
raise ValueError(f"{node.kind} Unable to find the generic {generic_kind}")
ValueError: BugrepoChild Unable to find the generic BugrepoParent
4. Observe persistent corruption
Re-running the baseline load (or any other schema operation):
infrahubctl schema load baseline.yml
Unable to load the schema:
Unable to find the Schema associated with <original-UUID>, BugrepoParent
The branch is now unrecoverable through infrahubctl / /api/schema/load.
Additional Information
Root cause analysis (from traces)
Two interacting defects:
- Incomplete reference check on delete. The delete-path validator checks
peer references but does not check inherit_from references. Once peer references are cleared (as in step 1), the generic appears deletable and passes pre-write validation.
- Non-atomic write + post-processing. In
update_schema_branch → load_schema_from_db, the Neo4j delete persists before schema.process() re-validates the fully-loaded graph. When process_inheritance raises, the delete is already committed. The error surfaces as a 500, but orphaned inherit_from edges remain in the graph referencing the deleted generic's UUID.
Recovery attempts that all fail through the public API
Tried against the corrupted branch from a reproducible instance:
| Attempt |
Result |
Re-declare the generic with its original UUID via id: in YAML |
Unable to find the Schema associated with <UUID>, <Generic> |
| Re-declare the generic without any UUID |
Same error |
state: absent on the orphaned inheriting node in isolation |
Rejected if any peer still references the inheritor |
state: absent on the orphaned inheritor + cleanup of peer relationships referencing it, all in one apply |
Node-level 'inherit_from' constraint violation on schema 'SchemaNode' |
Proposed fix direction
- Extend the delete-path validator to enumerate
inherit_from references symmetrically with peer references and reject deletes with live inheritors. Clear error, no DB touch.
- Wrap the Neo4j write +
schema.process() call inside update_schema_branch in a single transaction so validation failures in process_inheritance roll back the delete.
- Optionally ship an
infrahubctl schema repair or Cypher-level recovery path for databases already in this state.
Originating customer scenario
A shared dev instance hit this while attempting a migration — changing an attribute's kind from Number → NumberPool, which requires delete-and-re-add since kind is not editable in place. The path:
- Pushed a commit changing
kind: Number → kind: NumberPool. Silently rejected as invalid (kind cannot change in place). No UI indication that the schema push failed, which is its own usability issue worth filing separately.
- Pushed a commit reverting
kind: NumberPool → kind: Number, and in the same commit adding state: absent on the peer relationship pointing to the generic. Applied cleanly.
- Pushed a commit with
state: absent on the generic. Hit this bug.
Git integration amplifies the exposure because every commit is a separate schema apply — users naturally produce the two-step sequence that triggers the bug across two commits.
Environment
- Infrahub: 1.8.5
- Helm chart:
opsmill/infrahub 4.21.0 (appVersion overridden to 1.8.5 via global.infrahubTag)
- Dependencies: Neo4j 2025.10.1-4 community single-node, Redis 19.5.2, RabbitMQ 14.4.1, Prefect 3.6.8
- Cluster: KIND 0.31.0, Kubernetes 1.35.0
- Python runtime (from image): 3.13
- Trigger: two sequential
infrahubctl schema load calls on main in a fresh deploy
Reproduction artifacts (values override, schemas, server log tail) available on request.
Component
No response
Infrahub version
1.8.5
Current Behavior
Triggered by the following two-step sequence (each step a separate
POST /api/schema/load— either twoinfrahubctl schema loadcalls or two commits via git integration):state: absenton everypeer-kind relationship pointing at a generic. This clears allpeerreferences to the generic.inherit_fromreferences from other nodes are untouched. Apply succeeds.state: absenton the generic itself.On step 2:
The pre-write validator checks
peerreferences. After step 1 there are none, so validation passes.inherit_fromreferences are not checked.The delete is persisted to Neo4j.
Post-write,
schema.process() → process_inheritance()atbackend/infrahub/core/schema/schema_branch.py:1795raisesValueError: <Node> Unable to find the generic <Generic>because the inheriting node still carries aninherit_fromedge to the just-deleted generic.The client receives HTTP 500 Internal Server Error.
The delete is not rolled back. The schema graph is now corrupted.
Every subsequent call to
POST /api/schema/loadon the branch — including re-declaring the generic with or without the original UUID — fails with:The branch cannot be unwedged through the public API.
Note: if the same two mutations are applied in a single
POST /api/schema/load(peer removal and generic deletion together in one file), the validator catches the inconsistency and rejects it cleanly with<Node>: Relationship '<rel>' is referring an invalid peer '<Generic>'. The bug only manifests when the steps are split across two separate applies — which is the normal case for git integration, where each commit with schema changes is its own apply.Expected Behavior
On step 2 above, the delete validator should symmetrically check
inherit_fromreferences alongsidepeerreferences and reject the operation cleanly before any DB write, with a message like:The schema graph should remain unchanged.
Steps to Reproduce
A fresh Infrahub 1.8.5 instance with no prior data. Three YAML files, three
infrahubctl schema loadcalls. A trivial three-kind schema is sufficient.1. Load the baseline schema —
baseline.ymlinfrahubctl schema load baseline.yml # succeeds2. Remove the
peerreference fromBugrepoDevice—step1.ymlinfrahubctl schema load step1.yml # succeedsAfter this,
BugrepoParenthas nopeer:references. It still has oneinherit_from:reference fromBugrepoChild.3. Delete the generic —
step2.ymlReturns HTTP 500 Internal Server Error. Server traceback:
4. Observe persistent corruption
Re-running the baseline load (or any other schema operation):
The branch is now unrecoverable through
infrahubctl//api/schema/load.Additional Information
Root cause analysis (from traces)
Two interacting defects:
peerreferences but does not checkinherit_fromreferences. Once peer references are cleared (as in step 1), the generic appears deletable and passes pre-write validation.update_schema_branch → load_schema_from_db, the Neo4j delete persists beforeschema.process()re-validates the fully-loaded graph. Whenprocess_inheritanceraises, the delete is already committed. The error surfaces as a 500, but orphanedinherit_fromedges remain in the graph referencing the deleted generic's UUID.Recovery attempts that all fail through the public API
Tried against the corrupted branch from a reproducible instance:
id:in YAMLUnable to find the Schema associated with <UUID>, <Generic>state: absenton the orphaned inheriting node in isolationstate: absenton the orphaned inheritor + cleanup of peer relationships referencing it, all in one applyNode-level 'inherit_from' constraint violation on schema 'SchemaNode'Proposed fix direction
inherit_fromreferences symmetrically withpeerreferences and reject deletes with live inheritors. Clear error, no DB touch.schema.process()call insideupdate_schema_branchin a single transaction so validation failures inprocess_inheritanceroll back the delete.infrahubctl schema repairor Cypher-level recovery path for databases already in this state.Originating customer scenario
A shared dev instance hit this while attempting a migration — changing an attribute's
kindfromNumber→NumberPool, which requires delete-and-re-add sincekindis not editable in place. The path:kind: Number→kind: NumberPool. Silently rejected as invalid (kindcannot change in place). No UI indication that the schema push failed, which is its own usability issue worth filing separately.kind: NumberPool→kind: Number, and in the same commit addingstate: absenton the peer relationship pointing to the generic. Applied cleanly.state: absenton the generic. Hit this bug.Git integration amplifies the exposure because every commit is a separate schema apply — users naturally produce the two-step sequence that triggers the bug across two commits.
Environment
opsmill/infrahub4.21.0 (appVersion overridden to 1.8.5 viaglobal.infrahubTag)infrahubctl schema loadcalls onmainin a fresh deployReproduction artifacts (values override, schemas, server log tail) available on request.