siyuan-note/siyuan · error
database back relation value is invalid
Error message
database back relation value is invalid
What it means
After locating or creating the back-relation value for a target row, the code requires value.Relation (the ValueRelation struct) to be non-nil before appending the restored block ID. A relation-typed value with a nil Relation payload is internally inconsistent — the row claims to be a relation but carries no relation data — so the rollback aborts rather than dereference a nil pointer.
Solutions
- Fix or delete the malformed relation value in the destination database (re-set the relation on that row via the UI or API) so its Relation payload is populated, then retry the rollback
- Remove the null/malformed Relation entry from the destination .av JSON and let the restore recreate it (the code path creates a fresh value when GetValue returns nil)
- Roll back to an earlier state of the destination database where its relation values were intact
- Rebuild the two-way relation field in the destination database to regenerate well-formed values
Defensive patterns
Strategy: type-guard
Validate before calling
// Go: check relation payload before appending
backValue := back.GetValue(targetID)
if backValue != nil && backValue.Relation == nil {
// malformed value; repair before rollback
} Type guard
if backValue != nil && backValue.Relation != nil {
// safe to append block IDs
} Try / catch
err := tx.restoreBoundAttributeViewHistory(tree, historyDir)
if err != nil && strings.Contains(err.Error(), "back relation value is invalid") {
// repair the destination row's relation value, then retry
} Prevention
- Never hand-edit .av JSON value payloads
- Repair rows with empty relation values via the UI before rollback
- Guard plugin/API writes so relation values always carry a Relation struct
- Run integrity checks after crash recovery
When it happens
Trigger: RollbackDocHistory where the destination database's back-relation value for a target row exists (back.GetValue(targetID) != nil) but its Relation field is nil — i.e. corrupted or malformed value data in the live destination database, or a value created by a buggy writer that left the Relation struct empty.
Common situations: Destination database rows whose relation values were hand-edited or corrupted by crashes/sync; values written by older/plugin code paths that omitted the Relation payload; snapshots merged earlier with partially initialized relation values.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- database back relation changed since document deletion
- related database entry
- database back relation field
- database back relation values
- database entry [ ] has a different binding
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/500174ecf5a584e8.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/attribute_view_block_history.go:242
return err
}
back, err := dest.GetKeyValues(relation.BackKeyID)
if err != nil || back.Key.Relation == nil || !back.Key.Relation.IsTwoWay ||
back.Key.Relation.AvID != id || back.Key.Relation.BackKeyID != kv.Key.ID {
return errors.New("database back relation changed since document deletion")
}
for _, targetID := range value.Relation.BlockIDs {
if dest.GetBlockValue(targetID) == nil {
return fmt.Errorf("related database entry [%s] is missing", targetID)
}
backValue := back.GetValue(targetID)
if backValue == nil {
backValue = &av.Value{ID: ast.NewNodeID(), KeyID: back.Key.ID, BlockID: targetID,
Type: av.KeyTypeRelation, Relation: &av.ValueRelation{}}
back.Values = append(back.Values, backValue)
}
if backValue.Relation == nil {
return errors.New("database back relation value is invalid")
}
if !slices.Contains(backValue.Relation.BlockIDs, value.BlockID) {
backValue.Relation.BlockIDs = append(backValue.Relation.BlockIDs, value.BlockID)
}
}
}
}
}
if len(restoredItems) == 0 {
return nil
}
for _, view := range after {
regenAttrViewGroups(view)
}
return tx.saveAttributeViewFieldChanges(&attributeViewFieldsSnapshot{boxID: boxID}, before, after)
}
View on GitHub (pinned to 9f775e8a12)