Skip to content

fix(sheet): stop padding no-model rows one column past a supplied head - #1196

Open
Sean-Walker0 wants to merge 1 commit into
apache:mainfrom
Sean-Walker0:fix/no-model-explicit-head-phantom-column
Open

Sean-Walker0 wants to merge 1 commit into
apache:mainfrom
Sean-Walker0:fix/no-model-explicit-head-phantom-column

Conversation

@Sean-Walker0

@Sean-Walker0 Sean-Walker0 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Purpose of the pull request

No-model reads (doReadSync() returning Map<Integer, String> rows) that supply an explicit .head(...) return one phantom trailing null column on every row. A four-column head produces five-entry maps (keys 0..4); a short data row is padded to five entries as well. Any consumer that sizes output from the map (row.size(), values().toArray(), re-writing rows as arrays) gets a wrong-width row.

No issue exists for this yet; found by code audit while reviewing buildNoModel's padding loop.

What's changed?

ModelBuildEventListener.buildNoModel pads rows with while (index <= headSize), but calculateHeadSize answers a column count when a head is supplied, versus the last column index of the widest head row when no head is supplied:

  • with an explicit 4-column head and a full row (keys 0..3), the loop leaves index == 4 and 4 <= 4 stores one extra null under key 4;
  • the inclusive bound was introduced for the headless branch (fix for 读取不到最后一个空列的问题 #220, commit 8ad28c2), where calculateHeadSize returns an index — and it re-broke the explicit-head case that the loop's original exclusive bound had fixed.

The change makes calculateHeadSize answer a column count in both branches (maxNotEmptyDataHeadSize + 1 for the headless case) and uses an exclusive < bound. Explicit-head rows are now exactly head-wide; headless padding through the widest head column is unchanged.

Behavior notes:

  • a headless row with no cells at all now yields an empty map instead of a single phantom null column — same defect family, disclosed here for review;
  • no-model reads without any head information are unchanged.

NoModelHeadSizeTest covers both directions: exact width for explicit heads (full and short rows, no key 4) and unchanged headless padding to the widest head row.

Checklist

  • I have read the Contributor Guide.
  • I have written the necessary doc or comment.
  • I have added the necessary unit tests and all cases have passed.

Verifying this change

  • NoModelHeadSizeTest fails on unpatched code (expected: <4> but was: <5> on both explicit-head cases) and passes after the fix; the headless-pinning case passes before and after.
  • ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet: 919 tests, 0 failures, 0 errors.
  • ./mvnw -pl fesod-sheet spotless:check: clean.

Notes

@Sean-Walker0
Sean-Walker0 force-pushed the fix/no-model-explicit-head-phantom-column branch 2 times, most recently from 7745b09 to 3e5ce44 Compare October 10, 2026 12:22

@Mikkey-f Mikkey-f left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through this end to end and reproduced both sides locally — the analysis holds up.

Root cause, confirmed in source

calculateHeadSize really is answering two different questions, and the single bound papered over it:

  • supplied head → headMap.size() is a column count: ExcelHeadProperty inserts contiguous keys 0..n-1 for the supplied list;
  • no head → maxNotEmptyDataHeadSize is the last non-empty column index of the final head row: DefaultAnalysisEventProcessor.buildHead sets it per non-empty cell (last write wins on an ordered cell map).

<= is correct for an index and off by one for a count. Normalizing both branches to counts and keeping < is the right shape, and it keeps the fastexcel#220 fix intact.

Independent verification (JDK 17, Windows)

  • Red: with only the product file reverted to current main (bf9bf923) and your tests kept, NoModelHeadSizeTest fails exactly as you describe — 2 of 3, expected: <4> but was: <5> on both explicit-head cases; the headless pinning case passes on both sides.
  • Green: on your head all 3 pass.
  • fastexcel#220 guard: MaxHeadSizeTest (all three fixtures, including the "fewer head columns than data columns" one) passes 3/3 — headless padding is unchanged.
  • Full suite, CI-equivalent command: mvn clean package -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet → 60 + 1047 tests, 0 failures. spotless:check clean.

The disclosed empty-row change, checked

I built a file with POI (empty first row, data, then a truly empty row) and read it with ignoreEmptyRow(false): the empty row now returns {} where main returned {0=null}; with an explicit head it returns exactly head-width nulls (main returned width + 1). Both fall out of the same fix as intended. As you say, it only surfaces with ignoreEmptyRow(false) — RowTagHandler marks all-empty rows RowTypeEnum.EMPTY and endRow drops them while the flag is true (the default) — so one small test would pin it if you want it locked in, but it's optional.

Two small nits

  1. The new javadoc says "the widest head row answers with its last column index" — the value actually comes from the last head row only (buildHead runs a single time, when rowIndex + 1 == headRowNumber); for a multi-row head the two can differ, so "last head row" would be exact.
  2. Tiny description nit: in Notes, #1128's converterMap() → converterRegistry() rename is inside buildNoModel itself (line 86), not "other methods" of the file. No line overlap with your change either way — just a trivial rebase for whoever lands second.

Heads-up that the three workflow runs on this PR are still action_required (the fork approval gate), so CI hasn't executed yet; the numbers above come from rerunning the CI-equivalent command locally.

LGTM from my side — minimal change, both documented behaviors preserved, tests pin the exact fix.

@Sean-Walker0
Sean-Walker0 force-pushed the fix/no-model-explicit-head-phantom-column branch from 3e5ce44 to 1ca27ea Compare October 13, 2026 05:00
@Sean-Walker0

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough independent verification — all three points addressed and force-pushed as 1ca27ea:

  1. Javadoc now says "the last head row" instead of "the widest head row".
  2. The PR description Notes now places refactor: decouple read/write responsibilities and introduce ColumnBinding #1128's rename inside buildNoModel itself.
  3. Added the optional pin: NoModelHeadSizeTest.readWithIgnoreEmptyRowFalseReturnsEmptyMapForEmptyRows builds a POI file (empty head row, one data row, trailing empty row) and asserts the trailing empty row is delivered as an empty map with ignoreEmptyRow(false) — 4/4 green on this branch, and the case fails on main per the disclosed {0=null} behavior.

@Mikkey-f

Copy link
Copy Markdown
Contributor

Thanks @Sean-Walker0 — checked 1ca27ea and all three points look addressed; the pinned ignoreEmptyRow(false) case is a nice addition.

@Sean-Walker0
Sean-Walker0 force-pushed the fix/no-model-explicit-head-phantom-column branch from 1ca27ea to 1265e51 Compare October 14, 2026 12:13
buildNoModel pads each no-model row with null cells up to the head
size, but the loop bound was inclusive while calculateHeadSize answers
a column count when a head is supplied. Every row of a no-model read
with an explicit head therefore carried one phantom trailing null
column: a four-column head produced five-entry maps (keys 0..4).

The loop originally used an exclusive bound, which was later switched
to an inclusive one for the headless branch, where calculateHeadSize
answers the last column index of the widest head row instead of a
count (fix for apache#220, commit 8ad28c2) - and that
switch re-broke the explicit-head case the exclusive bound had fixed.

Make calculateHeadSize answer a column count in both branches and use
an exclusive bound. The explicit-head padding is fixed, while headless
rows keep being padded through the widest head column. A side effect
is that a headless row with no cells at all no longer reports a single
phantom null column.

Verified with NoModelHeadSizeTest (red before the change, green after)
plus the full fesod-sheet suite (919 tests) and spotless:check.
@Sean-Walker0
Sean-Walker0 force-pushed the fix/no-model-explicit-head-phantom-column branch from 1265e51 to 9222d90 Compare October 15, 2026 06:45

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants