ADR-060: Stable AG Grid row identity via row_index integer
Date: 2026-02-22 Status: Accepted
Context
AG Grid requires a getRowId callback to uniquely identify rows for stable
updates (cell edits, bulk updates). The original implementation used vm_name
as the row identity:
":getRowId": "params => params.data.vm_name"
Customer RVTools exports regularly contain duplicate VM names — linked clones,
template copies, and migrations all produce files where the same name appears
multiple times. When getRowId returns the same string for two rows, AG Grid
collapses them into a single logical row. Inline workload edits then silently
apply to the wrong VM.
Decision
Assign a stable integer row_index (0-based, contiguous) in ingest_file()
after template filtering and reset_index():
df["row_index"] = df.index.astype(int)
Switch getRowId to:
":getRowId": "params => String(params.data.row_index)"
String() is required — AG Grid expects a string from getRowId.
row_index is registered in CANONICAL_COLUMNS so it passes through the
parser whitelist. Parsers set a placeholder result["row_index"] = 0 before
return result[CANONICAL_COLUMNS]; the real value is overwritten in
ingest_file().
Both _handle_cell_change and _handle_bulk_update in review.py use
int(row.get("row_index", -1)) == row_idx for row matching instead of
string VM name comparison.
Consequences
- Positive: Duplicate VM names no longer corrupt row identity or cause edits to silently apply to the wrong VM.
- Positive:
row_indexdoubles as a stable join key for IOPS performance data merges. - Negative:
row_indexis internal — it is defined inCANONICAL_COLUMNSbut has no visible column definition in the grid (nocolumnDefentry), so it never appears as a user-visible column. - Neutral: Existing unit tests continued passing without modification
because tests that exercise
_handle_cell_changeconstruct records withrow_indexpopulated.