siyuan-note/siyuan · error
database entry [ ] changed since deletion was undone
Error message
database entry [%s] changed since deletion was undone
What it means
prepareAttributeViewItemRemoval, in replay (undo) mode, verifies that the row values recorded in the deletion snapshot (op.attributeViewItems.Keys) still match the live database exactly via reflect.DeepEqual before re-inserting them. If a recorded value differs from the current cell — meaning the entry was modified after the deletion was (or is being) undone — the kernel aborts with 'database entry [%s] changed since deletion was undone' to avoid clobbering newer data.
Solutions
- Undo in order: first undo the edits made after the deletion, then undo the row deletion
- Accept the current values and skip restoring the stale snapshot (re-add the row and re-enter lost values manually)
- Refresh/sync to a state consistent with the snapshot before replaying the undo
Example fix
// before
tx.undo(removeBlockOp) // assumes values unchanged since delete
// after
live := av.GetValue(current.KeyValues, kv.Key.ID, value.BlockID)
if !reflect.DeepEqual(value, live) {
// handle divergence: skip or merge before replaying the deletion undo
return nil
} Defensive patterns
Strategy: validation
Validate before calling
for _, kv := range snap.Keys {
for _, v := range kv.Values {
if !reflect.DeepEqual(v, av.GetValue(current.KeyValues, kv.Key.ID, v.BlockID)) {
return fmt.Errorf("entry %s diverged from snapshot; handle edits first", v.BlockID)
}
}
} Type guard
func snapshotFresh(current *av.AttributeView, snap *model.AttributeViewItemsSnapshot) bool {
for _, kv := range snap.Keys {
for _, v := range kv.Values {
if !reflect.DeepEqual(v, av.GetValue(current.KeyValues, kv.Key.ID, v.BlockID)) { return false }
}
}
return true
} Try / catch
if err := tx.prepareAttributeViewItemRemoval(op); err != nil {
if strings.Contains(err.Error(), "changed since deletion was undone") {
// merge newer edits manually or skip the undo step
return nil
}
return err
} Prevention
- Undo deletions before making further edits to the same rows
- Check for pending remote sync changes before replaying undos
- Avoid editing database cells via plugins while an undo is pending
When it happens
Trigger: Undo replay of doRemoveAttrViewBlock when, after the deletion snapshot was captured, one of the snapshot's values for a row no longer deep-equals the live value (row edited between original delete and undo, or the snapshot was replayed once already and the row changed).
Common situations: User deleted a row, edited remaining cells, then tried to undo the deletion; sync merged cell edits from another device into the same rows; a plugin updated values outside the snapshot's awareness.
Related errors
- database back relation values
- database entry [ ] has newer field values
- database field [ ] changed after the operation
- database entry [ ] was removed after the field
- database [ ]
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/c683f8f20e8ff5e1.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/attribute_view_items.go:54
Keys []*av.KeyValues
Orders map[string][]string
Covers map[string]map[string]*av.CardCoverPosition
relatedChanges *attributeViewFieldsSnapshot
}
func (tx *Transaction) prepareAttributeViewItemRemoval(op *Operation) error {
if tx.isReplay && nil != op.attributeViewItems {
current, err := tx.readAttributeViewItems(op.attributeViewItems)
if err != nil {
return err
}
if err = validateAttributeViewItemKeys(current, op.attributeViewItems); err != nil {
return err
}
for _, kv := range op.attributeViewItems.Keys {
for _, value := range kv.Values {
if !reflect.DeepEqual(value, av.GetValue(current.KeyValues, kv.Key.ID, value.BlockID)) {
return fmt.Errorf("database entry [%s] changed since deletion was undone", value.BlockID)
}
}
}
for _, kv := range current.KeyValues {
for _, value := range kv.Values {
if slices.Contains(op.attributeViewItems.ItemIDs, value.BlockID) &&
!reflect.DeepEqual(value, av.GetValue(op.attributeViewItems.Keys, kv.Key.ID, value.BlockID)) {
return fmt.Errorf("database entry [%s] has newer field values", value.BlockID)
}
}
}
if err = tx.prepareAttributeViewItemMutation(current, op.attributeViewItems); err != nil {
return err
}
return tx.replayAttributeViewItemConfiguration(op.attributeViewItems, false)
}
if tx.isReplay || !tx.fromAPI || len(tx.UndoOperations) == 0 {
return nilView on GitHub (pinned to 9f775e8a12)