siyuan-note/siyuan · warning

attribute view order changed; retry the drag

Error message

attribute view order changed; retry the drag

What it means

applyAttributeViewRowOrder is an optimistic-concurrency check: the client submits the row order it previewed plus the expected order it based the drag on. Before applying, the kernel re-serializes the current order (getAttributeViewRowOrder) and compares it with `change.Expected` using reflect.DeepEqual. Any difference — or a missing payload — rejects the drag so the client can retry against fresh data.

Source

Thrown at kernel/model/attribute_view_row_sort.go:232

			ret = append(ret, id)
			seen[id] = true
		}
	}
	return ret
}

func applyAttributeViewRowOrder(attrView *av.AttributeView, view *av.View, data any) error {
	encoded, err := gulu.JSON.MarshalJSON(data)
	if nil != err {
		return err
	}
	var change attributeViewRowOrderChange
	if err = gulu.JSON.UnmarshalJSON(encoded, &change); nil != err {
		return err
	}
	if nil == change.RowOrder || nil == change.Expected ||
		!reflect.DeepEqual(getAttributeViewRowOrder(view), change.Expected) {
		return errors.New("attribute view order changed; retry the drag")
	}
	if len(change.RowOrder.Groups) != len(view.Groups) {
		return errors.New("attribute view groups changed; retry the drag")
	}
	for _, group := range view.Groups {
		if _, ok := change.RowOrder.Groups[group.ID]; !ok {
			return errors.New("attribute view group not found")
		}
	}
	if nil != change.ValidateGroup && len(change.RowOrder.Sorts) > 0 {
		// 预览之后字段值可能变化,提交时再次校验,不能悄悄接受冲突的落点。
		copyView, copyErr := cloneAttributeViewRowSort(attrView)
		if nil != copyErr {
			return copyErr
		}
		candidate := copyView.GetView(view.ID)
		setAttributeViewRowOrder(candidate, change.RowOrder)
		collections, renderErr := renderAttributeViewRowSortCollections(copyView, candidate, false)

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Re-open the attribute view, rebuild the drag payload from the current row order, and retry
  2. Compare the submitted Expected order with the live order to identify what changed, and resolve the concurrent edit first
  3. Avoid long delays between drag preview and commit; apply the order promptly after preview

Example fix

// before
err = applyAttributeViewRowOrder(tx, avID, viewID, staleEncodedChange)
// after
change := buildRowOrderChange(view) // snapshot from the freshly loaded view
err = applyAttributeViewRowOrder(tx, avID, viewID, encode(change))
Defensive patterns

Strategy: retry

Validate before calling

const expected = getCurrentRowOrder(avID, viewID);
if (!deepEqual(expected, payload.Expected)) {
  payload = rebuildRowOrderChange(avID, viewID); // refresh before commit
}

Try / catch

try {
  await sortAttributeViewRow(avID, viewID, encodedChange);
} catch (e) {
  if (String(e.msg).includes("order changed")) {
    await reloadAttributeView(avID);
    await retryDragCommit(); // rebuild payload from fresh state
  }
}

Prevention

When it happens

Trigger: Calling sortAttributeViewRow with an encoded order change whose `Expected` snapshot no longer matches the live view — rows added/removed/reordered by another client, a sync write, a sort recomputation, or the client sending a payload computed from an outdated view.

Common situations: Multi-window editing of the same database; background sync updating rows between drag preview and commit; filter/sort recalculations that change effective order; replayed stale drag requests.

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@8641553a1f (2026-09-11). Data as JSON: /api/errors/8b80dd7f1c095a23. Report an issue: GitHub.