Skip to content

Commit 062f4c8

Browse files
pjfanningclaude
andauthored
XSSFSheet.shiftRows: stop rebuilding every row from the XML on each shift (#1300)
https://bz.apache.org/bugzilla/show_bug.cgi?id=70139 Since the fix for bug 64516, rebuildRows() reordered the CTRow array of the sheet (an XmlBeans deep copy of every row) and recreated every XSSFRow and XSSFCell, twice per shiftRows call. Inserting single rows into a sheet with a few thousand rows became orders of magnitude slower than POI 4, and the XSSFRow/XSSFCell instances held by the caller were silently invalidated. Shifting only renumbers rows, so the CTRow elements normally stay in order and rebuildRows() now just refreshes the keys of the _rows map, keeping the existing row and cell instances. It is a no-op when nothing was renumbered (shiftColumns). The XML reorder plus recreation is kept for the rare case where rows jump over other rows (bug 64516). Recreating the rows also re-registered shared and array formulas via onReadCell; that is now done explicitly by rebuildFormulaBookkeeping() at the end of shiftRows/shiftColumns. It also uncovered that updateRowFormulas shifted a shared formula ref twice when the sheet's registry holds the live formula of a re-homed master (after the old master cell was deleted) rather than a detached copy - the recreation used to paper over that. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 8c39881 commit 062f4c8

3 files changed

Lines changed: 185 additions & 2 deletions

File tree

poi-ooxml/src/main/java/org/apache/poi/xssf/usermodel/XSSFSheet.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,6 +3109,7 @@ public void shiftRows(int startRow, int endRow, final int n, boolean copyRowHeig
31093109
rowShifter.updateHyperlinks(formulaShifter);
31103110

31113111
rebuildRows();
3112+
rebuildFormulaBookkeeping();
31123113

31133114
for (XSSFTable table : overlappingTables) {
31143115
rebuildTableFormulas(table);
@@ -3147,6 +3148,7 @@ public void shiftColumns(int startColumn, int endColumn, final int n) {
31473148
columnShifter.updateNamedRanges(formulaShifter);
31483149

31493150
rebuildRows();
3151+
rebuildFormulaBookkeeping();
31503152

31513153
for (XSSFTable table : overlappingTables) {
31523154
rebuildTableFormulas(table);
@@ -3178,7 +3180,49 @@ private void rebuildTableFormulas(XSSFTable table) {
31783180
}
31793181
}
31803182

3183+
/**
3184+
* Brings the {@code _rows} map back in sync with the row numbers after rows were renumbered.
3185+
* <p>
3186+
* {@code _rows} is keyed by the row numbers the rows had before the shift, so it iterates the rows
3187+
* in the order of the CTRow elements in CTSheetData. Usually shifting keeps that order intact
3188+
* (rows are only renumbered), in which case only the keys of {@code _rows} need to be refreshed
3189+
* and the XSSFRow and XSSFCell instances stay valid. The XML only needs to be reordered (and the
3190+
* rows recreated) when rows jumped over other rows, see bug 64516.
3191+
*/
31813192
private void rebuildRows() {
3193+
XSSFRow[] rowArray = new XSSFRow[_rows.size()];
3194+
int[] rownums = new int[rowArray.length];
3195+
boolean renumbered = false;
3196+
boolean inOrder = true;
3197+
int i = 0;
3198+
for (Map.Entry<Integer, XSSFRow> entry : _rows.entrySet()) {
3199+
XSSFRow row = entry.getValue();
3200+
int rownum = row.getRowNum();
3201+
if (rownum != entry.getKey()) {
3202+
renumbered = true;
3203+
}
3204+
if (i > 0 && rownum <= rownums[i - 1]) {
3205+
inOrder = false;
3206+
break;
3207+
}
3208+
rowArray[i] = row;
3209+
rownums[i] = rownum;
3210+
i++;
3211+
}
3212+
if (!renumbered) {
3213+
return;
3214+
}
3215+
if (inOrder) {
3216+
_rows.clear();
3217+
for (i = 0; i < rowArray.length; i++) {
3218+
// Performance optimization: explicit boxing is slightly faster than auto-unboxing, though may use more memory
3219+
//noinspection UnnecessaryBoxing
3220+
final Integer rownumI = Integer.valueOf(rownums[i]); // NOSONAR
3221+
_rows.put(rownumI, rowArray[i]);
3222+
}
3223+
return;
3224+
}
3225+
31823226
//rebuild the CTSheetData CTRow order
31833227
SortedMap<Long, CTRow> ctRows = new TreeMap<>();
31843228
CTSheetData sheetData = getCTWorksheet().getSheetData();
@@ -3200,6 +3244,18 @@ private void rebuildRows() {
32003244
}
32013245
}
32023246

3247+
/**
3248+
* The shared and array formula bookkeeping refers to cell addresses and formula ranges,
3249+
* so it is refreshed from the cells once they and their formulas were shifted.
3250+
*/
3251+
private void rebuildFormulaBookkeeping() {
3252+
for (XSSFRow row : _rows.values()) {
3253+
for (Cell cell : row) {
3254+
onReadCell((XSSFCell) cell);
3255+
}
3256+
}
3257+
}
3258+
32033259
// remove all rows which will be overwritten
32043260
private void removeOverwritten(int startRow, int endRow, final int n) {
32053261
XSSFVMLDrawing vml = getVMLDrawing(false);

poi-ooxml/src/main/java/org/apache/poi/xssf/usermodel/helpers/XSSFRowColShifter.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,13 @@ static void updateNamedRanges(XSSFSheet sheet, FormulaShifter formulaShifter) {
113113
if(f.getT() == STCellFormulaType.SHARED){
114114
int si = Math.toIntExact(f.getSi());
115115
CTCellFormula sf = sheet.getSharedFormula(si);
116-
sf.setStringValue(shiftedFormula);
117-
updateRefInCTCellFormula(row, formulaShifter, sf);
116+
// the sheet usually holds a detached copy of the master formula, but after
117+
// the master cell was deleted it holds the live formula of the new master,
118+
// which is this very formula and must not be shifted a second time
119+
if (sf != null && sf != f) {
120+
sf.setStringValue(shiftedFormula);
121+
updateRefInCTCellFormula(row, formulaShifter, sf);
122+
}
118123
}
119124
}
120125

poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFSheetShiftRows.java

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ Licensed to the Apache Software Foundation (ASF) under one or more
2020
import static org.junit.jupiter.api.Assertions.assertEquals;
2121
import static org.junit.jupiter.api.Assertions.assertNotNull;
2222
import static org.junit.jupiter.api.Assertions.assertNull;
23+
import static org.junit.jupiter.api.Assertions.assertSame;
2324
import static org.junit.jupiter.api.Assertions.assertThrows;
25+
import static org.junit.jupiter.api.Assertions.assertTrue;
2426

2527
import java.io.IOException;
2628
import java.util.stream.IntStream;
@@ -38,6 +40,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
3840
import org.apache.poi.xssf.XSSFITestDataProvider;
3941
import org.apache.poi.xssf.XSSFTestDataSamples;
4042
import org.junit.jupiter.api.Test;
43+
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow;
4144

4245
public final class TestXSSFSheetShiftRows extends BaseTestSheetShiftRows {
4346

@@ -525,6 +528,125 @@ void test60709() throws IOException {
525528
wb.close();
526529
}
527530

531+
// bug 70139: shifting rows rebuilt every row and cell from the XML, which was slow and
532+
// invalidated the XSSFRow/XSSFCell instances the caller holds
533+
@Test
534+
void testShiftRowsKeepsRowAndCellInstances() throws IOException {
535+
try (XSSFWorkbook wb = new XSSFWorkbook()) {
536+
XSSFSheet sheet = wb.createSheet();
537+
for (int r = 0; r < 10; r++) {
538+
XSSFRow row = sheet.createRow(r);
539+
row.createCell(0).setCellValue("r" + r);
540+
row.setHeightInPoints(12 + r);
541+
}
542+
XSSFRow row3 = sheet.getRow(3);
543+
XSSFCell cell3 = row3.getCell(0);
544+
XSSFRow row9 = sheet.getRow(9);
545+
546+
sheet.shiftRows(3, 9, 2);
547+
548+
assertNull(sheet.getRow(3));
549+
assertNull(sheet.getRow(4));
550+
assertSame(row3, sheet.getRow(5));
551+
assertSame(cell3, sheet.getRow(5).getCell(0));
552+
assertEquals("r3", cell3.getStringCellValue());
553+
assertEquals(5, cell3.getRowIndex());
554+
assertEquals(15, row3.getHeightInPoints(), 0);
555+
assertSame(row9, sheet.getRow(11));
556+
assertEquals(11, sheet.getLastRowNum());
557+
assertRowsInOrder(sheet);
558+
559+
// the shifted row can be used further
560+
row3.createCell(1).setCellValue("added");
561+
XSSFRow inserted = sheet.createRow(3);
562+
inserted.createCell(0).setCellValue("inserted");
563+
assertRowsInOrder(sheet);
564+
565+
try (XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb)) {
566+
XSSFSheet sheet2 = wb2.getSheetAt(0);
567+
assertEquals("inserted", sheet2.getRow(3).getCell(0).getStringCellValue());
568+
assertNull(sheet2.getRow(4));
569+
assertEquals("r3", sheet2.getRow(5).getCell(0).getStringCellValue());
570+
assertEquals("added", sheet2.getRow(5).getCell(1).getStringCellValue());
571+
assertEquals(15, sheet2.getRow(5).getHeightInPoints(), 0);
572+
assertEquals("r9", sheet2.getRow(11).getCell(0).getStringCellValue());
573+
assertRowsInOrder(sheet2);
574+
}
575+
}
576+
}
577+
578+
// rows jumping over other rows (bug 64516) still need the XML rows to be reordered
579+
@Test
580+
void testShiftRowsOverOtherRowsKeepsSheetDataInOrder() throws IOException {
581+
try (XSSFWorkbook wb = new XSSFWorkbook()) {
582+
XSSFSheet sheet = wb.createSheet();
583+
for (int r = 0; r < 6; r++) {
584+
sheet.createRow(r).createCell(0).setCellValue("r" + r);
585+
}
586+
587+
// move row 5 on top of row 0, rows 1-4 are jumped over
588+
sheet.shiftRows(5, 5, -5);
589+
590+
assertRowsInOrder(sheet);
591+
assertEquals("r5", sheet.getRow(0).getCell(0).getStringCellValue());
592+
assertEquals("r1", sheet.getRow(1).getCell(0).getStringCellValue());
593+
assertNull(sheet.getRow(5));
594+
sheet.removeRow(sheet.getRow(0));
595+
assertEquals("r1", sheet.getRow(1).getCell(0).getStringCellValue());
596+
597+
try (XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb)) {
598+
XSSFSheet sheet2 = wb2.getSheetAt(0);
599+
assertNull(sheet2.getRow(0));
600+
assertEquals("r1", sheet2.getRow(1).getCell(0).getStringCellValue());
601+
assertEquals("r4", sheet2.getRow(4).getCell(0).getStringCellValue());
602+
assertRowsInOrder(sheet2);
603+
}
604+
}
605+
}
606+
607+
@Test
608+
void testShiftSharedFormulasTwice() throws Exception {
609+
try (XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("TestShiftRowSharedFormula.xlsx")) {
610+
XSSFSheet sheet = wb.getSheetAt(0);
611+
XSSFRow row5 = sheet.getRow(4);
612+
613+
sheet.shiftRows(3, sheet.getLastRowNum(), 1);
614+
assertSame(row5, sheet.getRow(5));
615+
assertEquals("SUM(C2:C5)", getCellFormula(sheet, "C6"));
616+
assertEquals("SUM(E3:E6)", getCellFormula(sheet, "E7"));
617+
618+
sheet.shiftRows(3, sheet.getLastRowNum(), 2);
619+
assertSame(row5, sheet.getRow(7));
620+
assertEquals("SUM(C2:C7)", getCellFormula(sheet, "C8"));
621+
assertEquals("SUM(D2:D7)", getCellFormula(sheet, "D8"));
622+
assertEquals("SUM(E3:E8)", getCellFormula(sheet, "E9"));
623+
assertRowsInOrder(sheet);
624+
625+
try (XSSFWorkbook wb2 = XSSFTestDataSamples.writeOutAndReadBack(wb)) {
626+
XSSFSheet sheet2 = wb2.getSheetAt(0);
627+
assertEquals("SUM(C2:C7)", getCellFormula(sheet2, "C8"));
628+
assertEquals("SUM(E3:E8)", getCellFormula(sheet2, "E9"));
629+
}
630+
}
631+
}
632+
633+
private static void assertRowsInOrder(XSSFSheet sheet) {
634+
long prev = 0;
635+
int count = 0;
636+
for (CTRow ctRow : sheet.getCTWorksheet().getSheetData().getRowList()) {
637+
assertTrue(ctRow.getR() > prev, "row " + ctRow.getR() + " after row " + prev);
638+
prev = ctRow.getR();
639+
count++;
640+
}
641+
assertEquals(sheet.getPhysicalNumberOfRows(), count);
642+
int i = 0;
643+
for (Row row : sheet) {
644+
assertSame(sheet.getRow(row.getRowNum()), row);
645+
i++;
646+
}
647+
assertEquals(count, i);
648+
}
649+
528650
@Test
529651
public void testBug69154() throws Exception {
530652
// this does not appear to work for HSSF but let's get it working for XSSF anyway

0 commit comments

Comments
 (0)