Skip to content

fix(xlsx): external-workbook references fall back to their cached value - #275

Open
JiaZhengOng wants to merge 1 commit into
iOfficeAI:mainfrom
JiaZhengOng:fix/external-ref-cached-fallback
Open

fix(xlsx): external-workbook references fall back to their cached value#275
JiaZhengOng wants to merge 1 commit into
iOfficeAI:mainfrom
JiaZhengOng:fix/external-ref-cached-fallback

Conversation

@JiaZhengOng

Copy link
Copy Markdown
Contributor

A formula referencing another workbook (=[1]Sheet!A1) can't be resolved without the linked file, so it surfaces as #REF! — which then poisons every total built on it (=SUM(...) over the column → #REF!). Excel keeps each such cell's last cached and computes totals from those, keeping the sheet usable.

This makes ResolveCellResult fall through to the cached value when an external-workbook reference (the [n] marker) evaluates to an error, instead of propagating #REF!. Genuine errors (no external link) still propagate.

Validation: across real workbooks with cross-file links, this turned 107 #REF!-cascade cells into their correct Excel values (spot-checked exact, e.g. =SUM(B19:B21) → 10609273.8). No regression — only external-ref error cells are touched.

Tradeoff (flagging for your call): the cached value can be stale if the linked file changed since last save. This mirrors Excel's own behavior (it shows the last cached external value until you refresh), so it's arguably the more faithful default — but noting it in case you'd rather keep the explicit #REF!.

A formula referencing another workbook (=[1]Sheet!A1) can't be resolved without the linked file and surfaces as #REF!, which poisons every dependent total. Excel keeps the cell's last cached <v>; fall through to it when an external ref errors, so SUMs over linked cells compute instead of cascading #REF!. Genuine (non-external) errors still propagate.
@goworm

goworm commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Thanks for this one — but I'm holding it pending a reproducing file, because I couldn't verify the bug on our engine.

I built synthetic cases with external-workbook refs (=[1]Ext!B1, including a real externalLinks part) and dependent totals, and also plain missing-sheet refs (=Missing!A1). In every case our evaluator returns null for the unresolved ref, which already falls through to the cached <v> — so main and this PR produce identical output; the #REF! cascade doesn't reproduce here. The guard only changes behaviour when the evaluator returns an actual error result for a [n]-containing formula, and I couldn't get it to (the engine has no external-ref resolution path, so [1]… never becomes #REF! — it becomes null).

Since it doesn't reproduce, I can't tell whether this is fixing a reachable bug or is a no-op on our current code — and the \[\d+\] guard would also swallow genuine errors in any external-ref formula.

Could you attach a minimal .xlsx that reproduces the #REF! cascade on officecli (ideally the smallest cut of one of those 107-cell workbooks)? With a file that actually triggers the error path I can confirm the fix and merge. 🙏

@JiaZhengOng

Copy link
Copy Markdown
Contributor Author

Thanks! And you've actually put your finger on it — [1]SUPPLIER!N131 is unquoted, so it returns null and correctly falls through to the cache. The cells that break are the quoted ones: Excel quotes any external sheet name containing a space, so '[5]TALLY JULY'!H3746 is quoted — and that form returns a #REF! error.

Here's a self-contained repro generator (pure stdlib) — save as build_repro.py, run it, and check the cell:

python3 build_repro.py repro.xlsx
officecli get repro.xlsx "ALL SUMMARY!B5"
  • B5 = SUM(B2:B4), where B2:B4 are '[1]TALLY JULY'!H100/H200/H300.
  • main: computedValue=#REF! (cachedValue 600) — view issues flags B5 formula_cache_stale.
  • this PR: computedValue=600, matching Excel's cached value.
build_repro.py
import zipfile, os, sys
OUT = sys.argv[1] if len(sys.argv) > 1 else "repro.xlsx"

PARTS = {
"[Content_Types].xml": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/externalLinks/externalLink1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml"/>
</Types>''',
"_rels/.rels": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>''',
"xl/workbook.xml": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets><sheet name="ALL SUMMARY" sheetId="1" r:id="rId1"/></sheets>
<externalReferences><externalReference r:id="rId2"/></externalReferences>
</workbook>''',
"xl/_rels/workbook.xml.rels": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink" Target="externalLinks/externalLink1.xml"/>
</Relationships>''',
"xl/worksheets/sheet1.xml": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="2"><c r="B2"><f>'[1]TALLY JULY'!H100</f><v>100</v></c></row>
<row r="3"><c r="B3"><f>'[1]TALLY JULY'!H200</f><v>200</v></c></row>
<row r="4"><c r="B4"><f>'[1]TALLY JULY'!H300</f><v>300</v></c></row>
<row r="5"><c r="B5"><f>SUM(B2:B4)</f><v>600</v></c></row>
</sheetData>
</worksheet>''',
"xl/externalLinks/externalLink1.xml": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<externalLink xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<externalBook r:id="rId1">
<sheetNames><sheetName val="TALLY JULY"/></sheetNames>
<sheetDataSet><sheetData sheetId="0">
<row r="100"><cell r="H100"><v>100</v></cell></row>
<row r="200"><cell r="H200"><v>200</v></cell></row>
<row r="300"><cell r="H300"><v>300</v></cell></row>
</sheetData></sheetDataSet>
</externalBook>
</externalLink>''',
"xl/externalLinks/_rels/externalLink1.xml.rels": '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath" Target="TALLY%20SOURCE.xlsx" TargetMode="External"/>
</Relationships>''',
}
if os.path.exists(OUT): os.remove(OUT)
with zipfile.ZipFile(OUT, "w", zipfile.ZIP_DEFLATED) as z:
    for name, data in PARTS.items(): z.writestr(name, data)
print("wrote", OUT)

Quoted vs unquoted on main:

formula in B2 B5 = SUM(B2:B4)
'[1]TALLY JULY'!H100 (quoted) #REF! ← cascades
[1]SUPPLIER!N131 (unquoted) falls through to cached ✓

One wrinkle that hid it: reading B2 directly returns null, but evaluating it inside SUM(B2:B4) returns #REF! — so it only surfaces on dependent totals.

Thanks also for the TimeSpan follow-up on #276 — good catch on the time-only text.

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