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
- Re-open both databases and verify the relation pair: source.Relation.{AvID,BackKeyID,IsTwoWay} and dest back field must mirror each other exactly
- Recreate the two-way relation (delete both one-way fields and re-add a single two-way relation field) to restore a consistent pair
- 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
- Never edit Relation fields or BackKeyID manually; use the relation UI/API
- Rebuild two-way relations as a pair when one side is orphaned
- Sync all devices fully before mutating relation definitions
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
- database back relation changed since document deletion
- database back relation value is invalid
- database back relation values
- related database entry
- restored database entry
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)