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

  1. Undo in order: first undo the edits made after the deletion, then undo the row deletion
  2. Accept the current values and skip restoring the stale snapshot (re-add the row and re-enter lost values manually)
  3. 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

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


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 nil

View on GitHub (pinned to 9f775e8a12)