Skip to content

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

Description

@PhillSimonds

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:

  1. The pre-write validator checks peer references. After step 1 there are none, so validation passes. inherit_from references are not checked.

  2. The delete is persisted to Neo4j.

  3. 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.

  4. The client receives HTTP 500 Internal Server Error.

  5. The delete is not rolled back. The schema graph is now corrupted.

  6. 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 BugrepoDevicestep1.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:

  1. 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.
  2. 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

  1. 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.
  2. 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.
  3. 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 NumberNumberPool, which requires delete-and-re-add since kind is not editable in place. The path:

  1. Pushed a commit changing kind: Numberkind: 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.
  2. Pushed a commit reverting kind: NumberPoolkind: Number, and in the same commit adding state: absent on the peer relationship pointing to the generic. Applied cleanly.
  3. 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.

Metadata

Metadata

Assignees

Labels

type/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