siyuan-note/siyuan · error

database back relation field

Error message

database back relation field [%s] has changed

What it means

When removing a two-way relation field, removeAttributeViewField loads the destination database named by relation.AvID and reads the back-relation field (relation.BackKeyID). It expects that back field to point back at the field being removed; if back.Relation.BackKeyID no longer equals key.ID, the mirrored pair has drifted out of sync and the kernel aborts the removal with this error instead of corrupting the other database.

Solutions

  1. Re-open both databases and verify the relation pair: source.Relation.{AvID,BackKeyID,IsTwoWay} and dest back field must mirror each other exactly
  2. Recreate the two-way relation (delete both one-way fields and re-add a single two-way relation field) to restore a consistent pair
  3. Retry the removal after the databases are consistent; if the back field is truly orphaned, remove the fields separately as one-way relations

Example fix

// before
key.Relation.BackKeyID = staleBackKeyID // manual edit desynced the pair
// after
backKey, _ := destAv.GetKey(key.Relation.BackKeyID)
if backKey != nil && backKey.Relation != nil && backKey.Relation.BackKeyID == key.ID {
    // pair is consistent; now safe to remove the field
}
Defensive patterns

Strategy: validation

Validate before calling

key, _ := srcAv.GetKey(fieldID)
if key != nil && key.Type == av.KeyTypeRelation && key.Relation != nil && key.Relation.IsTwoWay {
    dest, err := model.ParseAttributeView(key.Relation.AvID)
    if err != nil { return err }
    back, _ := dest.GetKey(key.Relation.BackKeyID)
    if back == nil || back.Relation == nil || back.Relation.BackKeyID != fieldID {
        // repair or recreate the two-way pair before removing
    }
}

Type guard

func isConsistentTwoWayPair(src, back *av.Key) bool {
    return src != nil && back != nil && src.Relation != nil && back.Relation != nil &&
        src.Relation.IsTwoWay && back.Relation.IsTwoWay && back.Relation.BackKeyID == src.ID
}

Try / catch

if err := tx.removeAttributeViewField(op); err != nil {
    if strings.Contains(err.Error(), "back relation field") {
        // desynced pair: rebuild the relation before retrying
    }
    return err
}

Prevention

When it happens

Trigger: Calling removeAttrViewColumn / RemoveAttributeViewKey on a two-way relation field while the opposite database's back-relation field has been modified (re-pointed, recreated, or edited by a concurrent sync/undo) so its Relation.BackKeyID does not reference the field being deleted.

Common situations: The two databases were synced from devices where one side edited the relation definition; a plugin rebuilt the back field manually; an earlier failed transaction left the two-way pair inconsistent; undo/redo replay restored one side but not the other.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/67d6a72360259f31. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/attribute_view_fields.go:122

	key, err := source.GetKey(op.ID)
	if err != nil {
		return err
	}
	if key.Type == av.KeyTypeBlock {
		return errors.New("cannot remove primary key field")
	}
	removals := map[string][]string{source.ID: {key.ID}}
	if relation := key.Relation; key.Type == av.KeyTypeRelation && relation != nil && relation.IsTwoWay && relation.AvID != "" {
		dest, loadErr := load(relation.AvID, true)
		if loadErr != nil {
			return loadErr
		}
		if dest != nil {
			back, _ := dest.GetKey(relation.BackKeyID)
			if back != nil && back.Type == av.KeyTypeRelation && back.Relation != nil &&
				back.Relation.AvID == source.ID && back.Relation.IsTwoWay {
				if back.Relation.BackKeyID != key.ID {
					return fmt.Errorf("database back relation field [%s] has changed", back.ID)
				}
				if op.RemoveDest {
					removals[dest.ID] = append(removals[dest.ID], back.ID)
				} else {
					back.Relation.IsTwoWay, back.Relation.BackKeyID = false, ""
					if dest.ID != source.ID || back.ID != key.ID {
						state.relationValues[dest.ID] = map[string]attributeViewRelationValues{back.ID: nil}
					}
				}
			}
		}
	}
	for id := range removals {
		for _, relatedID := range av.GetSrcAvIDs(id) {
			if _, err = load(relatedID, true); err != nil {
				return err
			}
		}

View on GitHub (pinned to 9f775e8a12)