siyuan-note/siyuan · error

invalid database field replay

Error message

invalid database field replay

What it means

replayAttributeViewFields replays a recorded field-level change snapshot during transaction undo/redo replay. It first verifies the replay is active and that the operation's AvID and field ID match the snapshot recorded when the original mutation ran (op.AvID == state.avID && op.ID == state.keyID); otherwise it rejects the operation with 'invalid database field replay' because the recorded changes cannot safely be applied to a different database or field.

Solutions

  1. Build undo/redo transactions through the official kernel helpers so the attributeViewFields snapshot is attached and IDs are preserved
  2. Do not modify AvID/ID of generated operations between the original execution and its replay
  3. Clear the corrupted undo stack (or re-sync) so replay starts from a consistent state

Example fix

// before
op.ID = otherFieldID // changed after snapshot capture
// after
// keep op.AvID/op.ID identical to the values captured in op.attributeViewFields
Defensive patterns

Strategy: try-catch

Validate before calling

if !tx.isReplay || op.attributeViewFields == nil || op.AvID != op.attributeViewFields.avID || op.ID != op.attributeViewFields.keyID {
    return fmt.Errorf("skip replay: snapshot mismatch")
}

Type guard

func snapshotMatches(op *model.Operation) bool {
    s := op.attributeViewFields
    return s != nil && op.AvID == s.avID && op.ID == s.keyID
}

Try / catch

if err := tx.replayAttributeViewFields(op); err != nil {
    if strings.Contains(err.Error(), "invalid database field replay") {
        // drop the stale undo entry instead of failing the whole transaction
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Undo/redo replay of doAddAttrViewColumn, doRemoveAttrViewColumn, doUpdateAttrViewColOptions, restoreDeletedAttributeViewBlocks, sortAttributeViewItem, or replayAttributeViewItemConfiguration where the operation's AvID or ID was mutated, or the snapshot (op.attributeViewFields) was not attached to the operation, or tx.isReplay is false when this path is entered.

Common situations: A plugin composed transaction operations manually and reordered or re-identified operations so the inverse no longer matches; an older client synced transactions produced by a newer kernel with different snapshot layout; snapshots dropped during serialization of a custom transaction.

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/558bbbba6cde7d33. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/attribute_view_fields.go:238

		layout.Filters = av.RemoveFiltersByColumn(layout.Filters, keyID)
		if len(layout.Filters) == 0 {
			layout.Filters = []*av.ViewFilter{{Combination: av.FilterCombinationAnd}}
		}
		layout.Sorts = slices.DeleteFunc(layout.Sorts, func(sort *av.ViewSort) bool { return sort.Column == keyID })
		if layout.Group != nil && layout.Group.Field == keyID {
			removeAttributeViewGroup0(layout)
			if layout.LayoutType == av.LayoutTypeKanban {
				// 看板必须保留有效分组,将自动选择的替代分组一并纳入撤销快照。
				setAttributeViewGroup(view, layout, &av.ViewGroup{Field: getKanbanPreferredGroupKey(view).ID})
			}
		}
	}
}

func (tx *Transaction) replayAttributeViewFields(op *Operation) error {
	state := op.attributeViewFields
	if !tx.isReplay || op.AvID != state.avID || op.ID != state.keyID {
		return errors.New("invalid database field replay")
	}
	if len(state.changes) == 0 {
		return nil
	}
	before, after := map[string]*av.AttributeView{}, map[string]*av.AttributeView{}
	for _, id := range sortedAttributeViewFieldKeys(state.changes) {
		current, err := tx.readAttributeViewForMutation(id, state.blockID, state.boxID)
		if err != nil {
			return err
		}
		for keyID, typ := range state.fieldTypes[id] {
			key, keyErr := current.GetKey(keyID)
			if keyErr != nil || key.Type != typ {
				return fmt.Errorf("database field [%s] changed after the operation", keyID)
			}
		}
		// 恢复双向定义时,另一端的单向值必须仍与删除时一致。
		for keyID, expected := range state.relationValues[id] {

View on GitHub (pinned to 9f775e8a12)