siyuan-note/siyuan · error

invalid database entry replay

Error message

invalid database entry replay

What it means

restoreAttributeViewItems re-inserts rows recorded in a deletion snapshot during undo replay. It first requires tx.isReplay to be true and op.AvID to match the snapshot's AvID (state.AvID); anything else — calling it outside replay, or an operation whose AvID was changed or detached from its snapshot — is rejected as 'invalid database entry replay' because the recorded rows cannot be safely restored into a different database or context.

Solutions

  1. Let the kernel capture the deletion snapshot automatically (do not hand-build insertAttrViewBlock inverses)
  2. Keep op.AvID identical to the snapshot's AvID throughout the transaction lifecycle
  3. Clear the corrupt undo entry (or rebuild the undo stack) and repeat the delete/undo cycle cleanly

Example fix

// before
op.AvID = otherAvID // retargeted inverse operation
// after
if op.AvID == snapshot.AvID && tx.isReplay {
    return tx.restoreAttributeViewItems(op)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if !tx.isReplay || op.attributeViewItems == nil || op.AvID != op.attributeViewItems.AvID {
    return fmt.Errorf("skip entry restore: snapshot/operation mismatch")
}

Type guard

func entrySnapshotValid(op *model.Operation) bool {
    s := op.attributeViewItems
    return s != nil && op.AvID == s.AvID
}

Try / catch

if err := tx.restoreAttributeViewItems(op); err != nil {
    if strings.Contains(err.Error(), "invalid database entry replay") {
        // drop the stale inverse operation and rebuild the undo stack
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: doInsertAttrViewBlock replay where the operation's AvID was altered between capture and replay, the attributeViewItems snapshot is missing or belongs to another database, or restoreAttributeViewItems is invoked with tx.isReplay false (direct call instead of replay).

Common situations: A plugin manually composes insertAttrViewBlock undo operations without the kernel-managed snapshot; operations reordered/re-targeted by custom transaction code; undo stack entries from a different workspace or synced from another client with different database IDs.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/cea784f5ff7e5cb9. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/attribute_view_items.go:285

			for _, value := range kv.Values {
				if !value.IsDetached && value.Block != nil {
					blockIDs = append(blockIDs, value.Block.ID)
				}
			}
		}
	}
	for _, id := range blockIDs {
		if err := tx.rememberAttributeViewMutationTree(id); err != nil {
			return err
		}
	}
	return nil
}

func (tx *Transaction) restoreAttributeViewItems(op *Operation) error {
	state := op.attributeViewItems
	if !tx.isReplay || op.AvID != state.AvID {
		return fmt.Errorf("invalid database entry replay")
	}
	current, err := tx.readAttributeViewItems(state)
	if err != nil {
		return err
	}
	for _, kv := range current.KeyValues {
		for _, value := range kv.Values {
			if slices.Contains(state.ItemIDs, value.BlockID) {
				return fmt.Errorf("database entry [%s] already exists", value.BlockID)
			}
		}
	}
	if err = validateAttributeViewItemKeys(current, state); err != nil {
		return err
	}
	for _, kv := range state.Keys {
		for _, value := range kv.Values {
			if value.Type != av.KeyTypeBlock || value.IsDetached || value.Block == nil {

View on GitHub (pinned to 9f775e8a12)