{"record":{"id":"67d6a72360259f31","repo":"siyuan-note/siyuan","slug":"database-back-relation-field-s-has-changed","errorCode":null,"errorMessage":"database back relation field [%s] has changed","messagePattern":"database back relation field \\[(.+?)\\] has changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/attribute_view_fields.go","lineNumber":122,"sourceCode":"\tkey, err := source.GetKey(op.ID)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif key.Type == av.KeyTypeBlock {\n\t\treturn errors.New(\"cannot remove primary key field\")\n\t}\n\tremovals := map[string][]string{source.ID: {key.ID}}\n\tif relation := key.Relation; key.Type == av.KeyTypeRelation && relation != nil && relation.IsTwoWay && relation.AvID != \"\" {\n\t\tdest, loadErr := load(relation.AvID, true)\n\t\tif loadErr != nil {\n\t\t\treturn loadErr\n\t\t}\n\t\tif dest != nil {\n\t\t\tback, _ := dest.GetKey(relation.BackKeyID)\n\t\t\tif back != nil && back.Type == av.KeyTypeRelation && back.Relation != nil &&\n\t\t\t\tback.Relation.AvID == source.ID && back.Relation.IsTwoWay {\n\t\t\t\tif back.Relation.BackKeyID != key.ID {\n\t\t\t\t\treturn fmt.Errorf(\"database back relation field [%s] has changed\", back.ID)\n\t\t\t\t}\n\t\t\t\tif op.RemoveDest {\n\t\t\t\t\tremovals[dest.ID] = append(removals[dest.ID], back.ID)\n\t\t\t\t} else {\n\t\t\t\t\tback.Relation.IsTwoWay, back.Relation.BackKeyID = false, \"\"\n\t\t\t\t\tif dest.ID != source.ID || back.ID != key.ID {\n\t\t\t\t\t\tstate.relationValues[dest.ID] = map[string]attributeViewRelationValues{back.ID: nil}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tfor id := range removals {\n\t\tfor _, relatedID := range av.GetSrcAvIDs(id) {\n\t\t\tif _, err = load(relatedID, true); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/attribute_view_fields.go#L104-L140","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nkey.Relation.BackKeyID = staleBackKeyID // manual edit desynced the pair\n// after\nbackKey, _ := destAv.GetKey(key.Relation.BackKeyID)\nif backKey != nil && backKey.Relation != nil && backKey.Relation.BackKeyID == key.ID {\n    // pair is consistent; now safe to remove the field\n}","handlingStrategy":"validation","validationCode":"key, _ := srcAv.GetKey(fieldID)\nif key != nil && key.Type == av.KeyTypeRelation && key.Relation != nil && key.Relation.IsTwoWay {\n    dest, err := model.ParseAttributeView(key.Relation.AvID)\n    if err != nil { return err }\n    back, _ := dest.GetKey(key.Relation.BackKeyID)\n    if back == nil || back.Relation == nil || back.Relation.BackKeyID != fieldID {\n        // repair or recreate the two-way pair before removing\n    }\n}","typeGuard":"func isConsistentTwoWayPair(src, back *av.Key) bool {\n    return src != nil && back != nil && src.Relation != nil && back.Relation != nil &&\n        src.Relation.IsTwoWay && back.Relation.IsTwoWay && back.Relation.BackKeyID == src.ID\n}","tryCatchPattern":"if err := tx.removeAttributeViewField(op); err != nil {\n    if strings.Contains(err.Error(), \"back relation field\") {\n        // desynced pair: rebuild the relation before retrying\n    }\n    return err\n}","preventionTips":["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"],"tags":["database","attribute-view","relation","consistency"],"backgroundTag":"invalid-state-transition","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}