siyuan-note/siyuan · error
related database entry
Error message
related database entry [%s] changed
What it means
When replaying an insert containing two-way relation values, each referenced target row must still exist in the destination database and carry a relation value whose back-link list can be extended. If the target row, its back-relation value, or that value's relation data is missing, the kernel cannot merge the back-link without losing the target's other relations, so it fails with this error.
Solutions
- Restore or re-create the missing related rows in the target database before replaying
- Undo the deletions that removed the referenced rows, then retry the transaction
- Re-add the relation links manually between the surviving rows
- Before scripted relation writes, verify each referenced row exists (GetBlockValue) and has a non-nil Relation value
Defensive patterns
Strategy: validation
Validate before calling
// confirm every related row exists with a relation value before linking
for (const relAvID of relationAvIDs) {
const av = await getAttrView(relAvID);
for (const id of relationBlockIDs) {
const row = av.keyValues.flatMap(kv => kv.values).find(v => v.blockID === id);
if (!row || !row.relation) throw new Error(`related entry ${id} missing`);
}
} Type guard
function hasRelationValue(row) { return !!row && !!row.relation && Array.isArray(row.relation.blockIDs); } Try / catch
try { await undo(); } catch (e) { if (/related database entry \[.+\] changed/.test(e.message)) { recreateRelatedRows(); retry(); } else throw e; } Prevention
- Do not delete rows referenced by two-way relations while replay/undo is pending
- Scripted deletions should first collect inbound relation references
- Re-add missing related rows before re-applying relation links
When it happens
Trigger: Replaying doInsertAttrViewBlock whose relation values point to rows that were deleted from the target database, whose relation values were cleared, or whose relation value lost its Relation payload since the snapshot.
Common situations: Target rows removed after the original insert (detected on undo/redo); a plugin cleared relation values; sync propagated deletions of the referenced rows.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- database back relation field
- database back relation changed since document deletion
- database back relation field
- database back relation value is invalid
- database back relation values
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/148e462899878a4e.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/attribute_view_items.go:356
if dest == nil {
dest, err = av.ParseAttributeViewForIndexInBox(kv.Key.Relation.AvID, state.BoxID)
if err != nil {
return err
}
if dest == nil {
return av.ErrViewNotFound
}
changed[dest.ID] = dest
}
back, backErr := dest.GetKeyValues(kv.Key.Relation.BackKeyID)
if backErr != nil || back.Key.Type != av.KeyTypeRelation || back.Key.Relation == nil ||
!back.Key.Relation.IsTwoWay || back.Key.Relation.AvID != state.AvID || back.Key.Relation.BackKeyID != kv.Key.ID {
return fmt.Errorf("database back relation field [%s] changed", kv.Key.Relation.BackKeyID)
}
for _, id := range value.Relation.BlockIDs {
backValue := back.GetValue(id)
if dest.GetBlockValue(id) == nil || backValue == nil || backValue.Relation == nil {
return fmt.Errorf("related database entry [%s] changed", id)
}
if !slices.Contains(backValue.Relation.BlockIDs, value.BlockID) {
backValue.Relation.BlockIDs = append(backValue.Relation.BlockIDs, value.BlockID)
}
}
}
}
for _, view := range current.Views {
view.ItemIDs = restoreAttributeViewItemOrder(view.ItemIDs, state.Orders[view.ID], state.ItemIDs)
for _, group := range view.Groups {
group.GroupItemIDs = restoreAttributeViewItemOrder(group.GroupItemIDs, state.Orders[group.ID], state.ItemIDs)
}
}
for id, covers := range state.Covers {
if covers != nil {
if current.CardCoverPositions == nil {
current.CardCoverPositions = map[string]map[string]*av.CardCoverPosition{}
}View on GitHub (pinned to 9f775e8a12)