iOfficeAI/OfficeCLI · error · ArgumentException

Invalid row index {rowIdx}. Valid row range is 1-1048576.

Error message

Invalid row index {rowIdx}. Valid row range is 1-1048576.

What it means

Excel worksheets support exactly 1,048,576 rows (2^20). AddRow resolves a 1-based target row index from --index/--before/--after anchors, or appends after the last existing RowIndex. When the resolved index falls outside [1, 1048576] this guard throws before any structural change, because emitting row[1048577+] produces an xlsx that Excel refuses to open. The append branch previously computed `lastRow + 1` silently past the ceiling; this mirrors the Set path's bound check so the overflow surfaces as a clean error at Add time.

Source

Thrown at src/officecli/Handlers/Excel/ExcelHandler.Add.Cells.cs:219

                return (int)uint.Parse(am.Groups[1].Value);
            }
            // For row insertion, --before /Sheet1/row[5] means "the new row
            // takes the row[5] slot, original row[5] shifts to row[6]". So
            // resolved index == anchor row number. --after /Sheet1/row[5]
            // means index == anchor + 1.
            if (position.Before != null) index = FindAnchorRow(position.Before);
            else index = FindAnchorRow(position.After!) + 1;
        }

        var rowIdx = index ?? ((int)(sheetData.Elements<Row>().LastOrDefault()?.RowIndex?.Value ?? 0) + 1);

        // Excel's row space tops out at 1048576 (2^20). The append branch
        // above silently produced row[1048577+] when row[1048576] already
        // existed, writing a file Excel rejects on open. Mirror the Set
        // path's bound check (ExcelHandler.Set.cs row index guard) so the
        // overflow surfaces as a clean invalid_value at Add time.
        if (rowIdx < 1 || rowIdx > 1048576)
            throw new ArgumentException(
                $"Invalid row index {rowIdx}. Valid row range is 1-1048576.");

        // If inserting at an existing position, shift everything at/below it
        // down. Gate only on "inserting at a position" (index set), NOT on the
        // presence of cell data at/below — sheet-level structures (CF / merge /
        // dataValidation) anchored on still-empty cells must shift too. Mirrors
        // AddCol, which calls ShiftColumnsRight on every positional insert
        // (CONSISTENCY(add-row-col-shift)). When nothing sits at/below rowIdx this
        // is a harmless no-op.
        // Validate all props BEFORE the structural shift (same atomicity rule
        // as AddCol): a height/outline parse failure after ShiftRowsDown left
        // the shift applied even though the add reported an error.
        double? parsedRowHeight = null;
        if (properties.TryGetValue("height", out var addRowHeight) && !string.IsNullOrWhiteSpace(addRowHeight))
            parsedRowHeight = ParseRowHeightPoints(addRowHeight);
        byte? parsedRowOutline = null;
        if (properties.TryGetValue("outline", out var addRowOutline)
            || properties.TryGetValue("outlinelevel", out addRowOutline)

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Read the sheet's last used row first; if it is already 1048576 you cannot append — create a second sheet and continue there.
  2. When using --index/--before/--after, compute the resulting 1-based row number (Index is 0-based internally then +1; Before anchor takes the anchor's slot, After = anchor+1) and confirm it is <= 1048576 before calling Add.
  3. For data exports approaching the ceiling, shard the data across multiple worksheets rather than relying on a single sheet.

Example fix

// before — appends when the sheet is full → throws
handler.Add("/Sheet1", "row", null, new() { ["cols"] = "3" });
// after — check capacity, spill to a new sheet
var lastRow = LastUsedRowIndex(handler, "/Sheet1");
var target = lastRow >= 1048576 ? "/Sheet2" : "/Sheet1";
if (target == "/Sheet2" && SheetExists(handler, "/Sheet2") == false)
    handler.Add("/", "sheet", null, new() { ["name"] = "Sheet2" });
handler.Add(target, "row", null, new() { ["cols"] = "3" });
Defensive patterns

Strategy: validation

Validate before calling

// Compute the 1-based row index AddRow will resolve, then bound-check it.
int ResolveTargetRow(ExcelHandler h, string sheet, InsertPosition pos)
{
    // Append case: last used row + 1
    var last = h.Query($"/{sheet}/row").Max(r => /* RowIndex */ 0);
    int idx = pos?.Index.HasValue == true ? pos.Index.Value + 1 : last + 1;
    if (pos?.After != null) idx = int.Parse(Regex.Match(pos.After, @"\[(\d+)\]").Groups[1].Value) + 1;
    if (pos?.Before != null) idx = int.Parse(Regex.Match(pos.Before, @"\[(\d+)\]").Groups[1].Value);
    return idx;
}
if (ResolveTargetRow(h, sheet, pos) is < 1 or > 1048576)
    throw new InvalidOperationException("Row index would exceed Excel's 1048576 ceiling.");

Try / catch

try { h.Add(sheet, "row", pos, props); }
catch (ArgumentException ex) when (ex.Message.Contains("Valid row range is 1-1048576"))
{ /* spill to a new sheet or skip */ }

Prevention

When it happens

Trigger: Add("/Sheet1","row",position,props) where position.Index+1 > 1048576 (e.g. InsertPosition.AtIndex(1048575) → index+1=1048576 is fine, but 1048576 → 1048577 throws); appending to a sheet whose last RowIndex is already 1048576 (auto-append computes lastRow+1=1048577); a --after anchor on /Sheet1/row[1048576] (resolves to 1048576+1).

Common situations: Bulk-export pipelines that stream rows into a single sheet without a ceiling check; batch replays onto a sheet that is already full; scripts that compute `--index` from an external counter that has overrun the Excel grid.

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/133c399a32bc64c0. Report an issue: GitHub.