siyuan-note/siyuan · warning

attribute view rows changed; retry the drag

Error message

attribute view rows changed; retry the drag

What it means

moveAttributeViewSortedRows filters the current row order into `moved` (selected rows) and `remaining`. If the number of matched selected rows differs from the number of distinct selected IDs, some selected rows no longer exist in the ordered list — the attribute view changed under the drag. The operation is rejected so the client retries with fresh data.

Source

Thrown at kernel/model/attribute_view_row_sort.go:183

func moveAttributeViewSortedRows(ordered, selected []string, previousID, nextID string) ([]string, error) {
	selectedIDs := map[string]bool{}
	for _, id := range selected {
		selectedIDs[id] = true
	}
	if 0 == len(selectedIDs) {
		return nil, errors.New("no attribute view rows selected")
	}
	var moved, remaining []string
	for _, id := range ordered {
		if selectedIDs[id] {
			moved = append(moved, id)
		} else {
			remaining = append(remaining, id)
		}
	}
	if len(moved) != len(selectedIDs) {
		return nil, errors.New("attribute view rows changed; retry the drag")
	}
	if selectedIDs[nextID] || ("" == nextID && selectedIDs[previousID]) {
		return slices.Clone(ordered), nil
	}
	index := 0
	if "" != nextID {
		index = slices.Index(remaining, nextID)
	} else if "" != previousID {
		index = slices.Index(remaining, previousID)
		if index >= 0 {
			index++
		}
	}
	if index < 0 {
		return nil, errors.New("attribute view drop target changed; retry the drag")
	}
	ret := append([]string{}, remaining[:index]...)
	ret = append(ret, moved...)

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Reload the attribute view row order and re-capture the selection, then retry the drag (the error message explicitly asks for a retry)
  2. Reconcile the selection against current rows: drop selected IDs that no longer exist and re-submit
  3. Check for concurrent editors/sync activity and re-attempt after the view stabilizes

Example fix

// before
rows, err := moveAttributeViewSortedRows(ordered, selected, prevID, nextID)
if err != nil { return err }
// after
rows, err := moveAttributeViewSortedRows(ordered, selected, prevID, nextID)
if err != nil {
    ordered, selected = refreshRowOrderAndSelection(avID) // re-read current state
    rows, err = moveAttributeViewSortedRows(ordered, selected, prevID, nextID)
}
Defensive patterns

Strategy: retry

Validate before calling

const currentIds = getVisibleRowIds(avID, viewID);
const stillValid = selected.every(id => currentIds.includes(id));
if (!stillValid) { refreshSelection(); }

Try / catch

try {
  await sortAttributeViewRows(avID, viewID, selected, prevID, nextID);
} catch (e) {
  if (String(e.msg).includes("attribute view rows changed")) {
    await reloadAttributeView(avID);
    // retry once with fresh selection
  }
}

Prevention

When it happens

Trigger: Calling prepareAttributeViewRowSort when one or more selected row IDs are absent from the view's current row order — rows deleted by another client/tab, block IDs changed after a transaction, or the drag started against a stale view snapshot.

Common situations: Two editors open on the same database, one deletes a row while the other drags; sync applied remote row deletions between drag start and commit; a plugin removed rows concurrently.

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