Skip to content

Commit 5af46a9

Browse files
committed
Optimize XLSX streaming queries and refresh .NET benchmark results
1 parent c1a4839 commit 5af46a9

3 files changed

Lines changed: 140 additions & 61 deletions

File tree

‎docs/dotnet-v1-query-benchmark.md‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,54 +43,54 @@ The following result was captured on 2026-08-26:
4343
| .NET SDK | 10.0.103 |
4444
| .NET MiniExcel | 1.46.1, commit `8b6feb87cfd00d0802de91bfca5616ec2dd744b7` |
4545
| Rust toolchain | rustc 1.85.0 (`4d91de4e4`, 2025-02-17) |
46-
| MiniExcel Rust base revision | `808828f3ce892dad8b00bda1cee370fae6451e1c` |
46+
| MiniExcel Rust | Source tree containing this document; optimization baseline `808828f3ce892dad8b00bda1cee370fae6451e1c` |
4747

4848
## Summary
4949

5050
Both implementations returned exactly 100,000 rows and 1,000,000 cells per pass in every measured process.
5151

5252
| Scenario | Runtime | Median Query time | Throughput | Median process time | Median peak working set | Maximum peak working set |
5353
| --- | --- | ---: | ---: | ---: | ---: | ---: |
54-
| Cold | .NET v1 | 1,890.76 ms | 52,889 rows/s | 1,963.50 ms | 67.22 MB | 68.12 MB |
55-
| Cold | Rust | 1,455.79 ms | 68,691 rows/s | 1,473.66 ms | 9.71 MB | 9.77 MB |
56-
| Steady | .NET v1 | 2,585.18 ms | 116,046 rows/s | 4,499.20 ms | 78.75 MB | 82.07 MB |
57-
| Steady | Rust | 4,209.62 ms | 71,265 rows/s | 5,728.73 ms | 9.91 MB | 9.94 MB |
54+
| Cold | .NET v1 | 1,714.86 ms | 58,314 rows/s | 1,769.77 ms | 66.84 MB | 68.19 MB |
55+
| Cold | Rust | 674.59 ms | 148,238 rows/s | 681.71 ms | 9.64 MB | 9.72 MB |
56+
| Steady | .NET v1 | 2,330.46 ms | 128,730 rows/s | 4,069.78 ms | 80.35 MB | 84.28 MB |
57+
| Steady | Rust | 2,079.01 ms | 144,299 rows/s | 2,792.32 ms | 9.88 MB | 9.93 MB |
5858

59-
For the first Query in a fresh process, Rust delivered 1.30x the .NET v1 throughput, completed in 23.0% less Query time, and used 85.6% less peak working set.
59+
For the first Query in a fresh process, Rust delivered 2.54x the .NET v1 throughput, completed in 60.7% less Query time, and used 85.6% less peak working set.
6060

61-
After an in-process warm-up, Rust delivered 0.61x the .NET v1 throughput and took 62.8% more Query time. Its median peak working set remained 87.4% lower, at 9.91 MB versus 78.75 MB.
61+
After an in-process warm-up, Rust delivered 1.12x the .NET v1 throughput and completed in 10.8% less Query time. Its median peak working set was 87.7% lower, at 9.88 MB versus 80.35 MB.
6262

63-
The result is therefore workload-dependent: Rust has lower first-call latency and substantially lower memory use here, while .NET v1 has higher sustained throughput after JIT warm-up.
63+
Relative to the Rust optimization baseline, median Query time decreased by 53.7% in the Cold scenario and 50.6% in the Steady scenario without increasing peak working set.
6464

6565
## Cold Results
6666

6767
| Iteration | .NET v1 elapsed | .NET v1 peak | Rust elapsed | Rust peak |
6868
| ---: | ---: | ---: | ---: | ---: |
69-
| 1 | 2,063.54 ms | 65.97 MB | 1,571.51 ms | 9.66 MB |
70-
| 2 | 1,950.17 ms | 67.22 MB | 1,397.59 ms | 9.75 MB |
71-
| 3 | 1,890.76 ms | 68.12 MB | 1,461.67 ms | 9.77 MB |
72-
| 4 | 1,816.06 ms | 67.63 MB | 1,394.83 ms | 9.71 MB |
73-
| 5 | 1,793.67 ms | 66.70 MB | 1,455.79 ms | 9.67 MB |
69+
| 1 | 1,721.06 ms | 66.84 MB | 672.67 ms | 9.63 MB |
70+
| 2 | 1,709.83 ms | 66.30 MB | 673.35 ms | 9.64 MB |
71+
| 3 | 1,714.86 ms | 66.03 MB | 674.59 ms | 9.64 MB |
72+
| 4 | 1,657.00 ms | 68.19 MB | 707.11 ms | 9.69 MB |
73+
| 5 | 1,756.68 ms | 67.51 MB | 704.74 ms | 9.72 MB |
7474

7575
## Steady Results
7676

7777
Each timed value below covers three complete Query passes after one untimed warm-up pass.
7878

7979
| Iteration | .NET v1 elapsed | .NET v1 peak | Rust elapsed | Rust peak |
8080
| ---: | ---: | ---: | ---: | ---: |
81-
| 1 | 2,401.10 ms | 79.22 MB | 4,209.62 ms | 9.92 MB |
82-
| 2 | 2,602.98 ms | 78.44 MB | 4,128.13 ms | 9.91 MB |
83-
| 3 | 2,554.63 ms | 82.07 MB | 4,357.14 ms | 9.89 MB |
84-
| 4 | 2,766.50 ms | 78.75 MB | 4,205.49 ms | 9.94 MB |
85-
| 5 | 2,585.18 ms | 78.27 MB | 4,254.68 ms | 9.80 MB |
81+
| 1 | 2,408.23 ms | 80.28 MB | 2,143.49 ms | 9.86 MB |
82+
| 2 | 2,349.56 ms | 84.28 MB | 2,294.03 ms | 9.90 MB |
83+
| 3 | 2,325.04 ms | 79.41 MB | 2,079.01 ms | 9.82 MB |
84+
| 4 | 2,308.75 ms | 80.35 MB | 2,011.56 ms | 9.93 MB |
85+
| 5 | 2,330.46 ms | 80.45 MB | 2,013.23 ms | 9.88 MB |
8686

8787
## Interpretation
8888

8989
The benchmark workbook contains 1,000,000 shared-string cells, 100,000 unique strings, and no merged ranges. Its worksheet XML expands to 39,167,847 bytes.
9090

91-
Rust currently performs a complete worksheet scan before the emitting pass, even though this workbook has a valid `<dimension>` and merged-cell filling is disabled. It also clones each shared-string value into an intermediate `Data::String` and then again into the public `CellValue::String`, clones dynamic column names for every row, and transfers each parsed row through a bounded synchronous channel. These choices preserve bounded memory and iterator ownership but add steady-state work.
91+
Rust now uses a valid `<dimension>` as the query extent when merged-cell filling does not require a complete worksheet scan. An explicit end cell skips the preliminary extent read as well. The parser reads hot row and cell attributes in one borrowed pass, parses shared-string indices without temporary `String` allocations, preallocates bounded row storage from the selected width, and moves owned string data from the intermediate `Data::String` into the public `CellValue::String`.
9292

93-
.NET v1 can stop its preliminary extent check as soon as it reads `<dimension ref="A1:J100000">`. Its JIT cost makes the first Query slower, but later Query passes benefit from already compiled and dynamically optimized hot paths. This explains why Rust leads in the cold scenario while .NET v1 leads in the steady scenario.
93+
The remaining per-row work includes cloning owned dynamic column names and transferring parsed rows through a bounded synchronous channel. These costs preserve the current `IndexMap<String, CellValue>` API, bounded memory, and iterator ownership. In this workbook, the optimized Rust path leads .NET v1 in both first-call latency and warmed sustained throughput while retaining substantially lower peak memory.
9494

9595
These numbers describe one workbook and one machine, not a general performance guarantee. The harness does not pin CPU affinity or suppress all operating-system noise; the median limits the effect of outliers. Use representative workbooks before drawing application-specific conclusions.
9696

‎docs/dotnet-v1-query-benchmark.zh-CN.md‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,54 +43,54 @@
4343
| .NET SDK | 10.0.103 |
4444
| .NET MiniExcel | 1.46.1,commit `8b6feb87cfd00d0802de91bfca5616ec2dd744b7` |
4545
| Rust toolchain | rustc 1.85.0(`4d91de4e4`,2025-02-17) |
46-
| MiniExcel Rust 基准 revision | `808828f3ce892dad8b00bda1cee370fae6451e1c` |
46+
| MiniExcel Rust | 包含本文档的源码树;优化基线为 `808828f3ce892dad8b00bda1cee370fae6451e1c` |
4747

4848
## 汇总结果
4949

5050
每个计量进程中,两种实现每次读取都准确返回 100,000 行、1,000,000 个单元格。
5151

5252
| 场景 | Runtime | Query 耗时中位数 | 吞吐量 | 总进程耗时中位数 | 峰值工作集中位数 | 最大峰值工作集 |
5353
| --- | --- | ---: | ---: | ---: | ---: | ---: |
54-
| Cold | .NET v1 | 1,890.76 ms | 52,889 行/秒 | 1,963.50 ms | 67.22 MB | 68.12 MB |
55-
| Cold | Rust | 1,455.79 ms | 68,691 行/秒 | 1,473.66 ms | 9.71 MB | 9.77 MB |
56-
| Steady | .NET v1 | 2,585.18 ms | 116,046 行/秒 | 4,499.20 ms | 78.75 MB | 82.07 MB |
57-
| Steady | Rust | 4,209.62 ms | 71,265 行/秒 | 5,728.73 ms | 9.91 MB | 9.94 MB |
54+
| Cold | .NET v1 | 1,714.86 ms | 58,314 行/秒 | 1,769.77 ms | 66.84 MB | 68.19 MB |
55+
| Cold | Rust | 674.59 ms | 148,238 行/秒 | 681.71 ms | 9.64 MB | 9.72 MB |
56+
| Steady | .NET v1 | 2,330.46 ms | 128,730 行/秒 | 4,069.78 ms | 80.35 MB | 84.28 MB |
57+
| Steady | Rust | 2,079.01 ms | 144,299 行/秒 | 2,792.32 ms | 9.88 MB | 9.93 MB |
5858

59-
对于新进程中的首次 Query,Rust 吞吐量为 .NET v1 的 1.30 倍,Query 耗时少 23.0%,峰值工作集少 85.6%。
59+
对于新进程中的首次 Query,Rust 吞吐量为 .NET v1 的 2.54 倍,Query 耗时少 60.7%,峰值工作集少 85.6%。
6060

61-
进程内 warm-up 后,Rust 吞吐量为 .NET v1 的 0.61 倍,Query 耗时多 62.8%;峰值工作集中位数仍低 87.4%,为 9.91 MB 对 78.75 MB。
61+
进程内 warm-up 后,Rust 吞吐量为 .NET v1 的 1.12 倍,Query 耗时少 10.8%;峰值工作集中位数低 87.7%,为 9.88 MB 对 80.35 MB。
6262

63-
因此结论取决于工作负载:在本次测试中,Rust 的首次调用延迟更低且内存明显更少;.NET v1 在 JIT 预热后的持续吞吐更高。
63+
与 Rust 优化基线相比,Cold 场景的 Query 耗时中位数下降 53.7%,Steady 场景下降 50.6%,且峰值工作集没有增加。
6464

6565
## Cold 逐轮结果
6666

6767
| 轮次 | .NET v1 耗时 | .NET v1 峰值 | Rust 耗时 | Rust 峰值 |
6868
| ---: | ---: | ---: | ---: | ---: |
69-
| 1 | 2,063.54 ms | 65.97 MB | 1,571.51 ms | 9.66 MB |
70-
| 2 | 1,950.17 ms | 67.22 MB | 1,397.59 ms | 9.75 MB |
71-
| 3 | 1,890.76 ms | 68.12 MB | 1,461.67 ms | 9.77 MB |
72-
| 4 | 1,816.06 ms | 67.63 MB | 1,394.83 ms | 9.71 MB |
73-
| 5 | 1,793.67 ms | 66.70 MB | 1,455.79 ms | 9.67 MB |
69+
| 1 | 1,721.06 ms | 66.84 MB | 672.67 ms | 9.63 MB |
70+
| 2 | 1,709.83 ms | 66.30 MB | 673.35 ms | 9.64 MB |
71+
| 3 | 1,714.86 ms | 66.03 MB | 674.59 ms | 9.64 MB |
72+
| 4 | 1,657.00 ms | 68.19 MB | 707.11 ms | 9.69 MB |
73+
| 5 | 1,756.68 ms | 67.51 MB | 704.74 ms | 9.72 MB |
7474

7575
## Steady 逐轮结果
7676

7777
以下每个计时值都包含一次不计时 warm-up 后的三次完整 Query。
7878

7979
| 轮次 | .NET v1 耗时 | .NET v1 峰值 | Rust 耗时 | Rust 峰值 |
8080
| ---: | ---: | ---: | ---: | ---: |
81-
| 1 | 2,401.10 ms | 79.22 MB | 4,209.62 ms | 9.92 MB |
82-
| 2 | 2,602.98 ms | 78.44 MB | 4,128.13 ms | 9.91 MB |
83-
| 3 | 2,554.63 ms | 82.07 MB | 4,357.14 ms | 9.89 MB |
84-
| 4 | 2,766.50 ms | 78.75 MB | 4,205.49 ms | 9.94 MB |
85-
| 5 | 2,585.18 ms | 78.27 MB | 4,254.68 ms | 9.80 MB |
81+
| 1 | 2,408.23 ms | 80.28 MB | 2,143.49 ms | 9.86 MB |
82+
| 2 | 2,349.56 ms | 84.28 MB | 2,294.03 ms | 9.90 MB |
83+
| 3 | 2,325.04 ms | 79.41 MB | 2,079.01 ms | 9.82 MB |
84+
| 4 | 2,308.75 ms | 80.35 MB | 2,011.56 ms | 9.93 MB |
85+
| 5 | 2,330.46 ms | 80.45 MB | 2,013.23 ms | 9.88 MB |
8686

8787
## 结果解释
8888

8989
测试工作簿包含 1,000,000 个 shared-string 单元格、100,000 个唯一字符串,且没有 merged range;worksheet XML 解压后为 39,167,847 bytes。
9090

91-
Rust 当前会在正式输出前完整扫描一次 worksheet,即使文件包含有效 `<dimension>` 且未启用 merged-cell 填充。读取 shared string 时,它还会先将值克隆为中间 `Data::String`,再克隆为公开 `CellValue::String`;动态列名也会逐行克隆,并且每行通过有界同步 channel 传递。这些设计保持了有界内存和 iterator 所有权安全,但增加了稳态工作量。
91+
Rust 现在会在不需要 merged-cell 填充时使用有效 `<dimension>` 作为 Query 范围;显式指定结束单元格时还会跳过初步范围读取。解析器以单次借用扫描读取高频 row/cell 属性,无临时 `String` 分配地解析 shared-string 索引,按所选列宽进行有上限的行存储预分配,并将中间 `Data::String` 的所有权直接移动到公开 `CellValue::String`。
9292

93-
.NET v1 在读取 `<dimension ref="A1:J100000">` 后即可结束范围预检查。首次 Query 的 JIT 成本使它在 Cold 场景较慢,但后续 Query 会受益于已编译及动态优化的热点路径。因此 Rust 在 Cold 场景领先,而 .NET v1 在 Steady 场景领先。
93+
剩余的逐行工作包括克隆动态列名,以及通过有界同步 channel 传递已解析行。这些成本维持了当前 `IndexMap<String, CellValue>` API、有界内存和 iterator 所有权语义。在该工作簿上,优化后的 Rust 在首次调用延迟和预热后的持续吞吐两方面均领先 .NET v1,同时保持明显更低的峰值内存。
9494

9595
这些数字只代表一份工作簿和一台机器,不是普遍性能保证。测试没有固定 CPU affinity,也无法消除所有操作系统噪声;使用中位数可以减弱异常值影响。针对具体应用做判断前,应使用有代表性的工作簿重新运行测试。
9696

‎miniexcel/src/streaming/ooxml.rs‎

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::{
2626
use super::shared_strings::SharedStrings;
2727

2828
const ROW_BUFFER_SIZE: usize = 8;
29+
const MAX_PREALLOCATED_ROW_CELLS: usize = 256;
2930

3031
pub(super) struct StreamingRawRows {
3132
receiver: Option<Receiver<Result<SelectedRow>>>,
@@ -1136,6 +1137,7 @@ struct CellState {
11361137
column: usize,
11371138
style: u32,
11381139
kind: CellKind,
1140+
shared_string_index: Option<usize>,
11391141
value: String,
11401142
inline_text: String,
11411143
formula: Option<String>,
@@ -1426,6 +1428,9 @@ where
14261428
let mut in_sheet_data = false;
14271429
let end_row = options.end_cell().map_or(scan.extent.end_row, |cell| Some(cell.row()));
14281430
let end_column = options.end_cell().map_or(scan.extent.end_column, |cell| Some(cell.column()));
1431+
let row_cell_capacity = end_column
1432+
.and_then(|column| column.checked_sub(options.start_cell().column()))
1433+
.map_or(0, |width| width.saturating_add(1).min(MAX_PREALLOCATED_ROW_CELLS));
14291434
let mut merge_fill = (options.fill_merged_cells() && !context.preserve_structure)
14301435
.then(|| MergeFillState::new(scan.merged_cells));
14311436

@@ -1457,7 +1462,11 @@ where
14571462
return Ok(());
14581463
}
14591464
last_declared_row = Some(excel_row);
1460-
current_row = Some(RowState { excel_row, cells: Vec::new(), next_column: 0 });
1465+
current_row = Some(RowState {
1466+
excel_row,
1467+
cells: Vec::with_capacity(row_cell_capacity),
1468+
next_column: 0,
1469+
});
14611470
}
14621471
Event::Empty(event) if in_sheet_data && is_name(event.name().as_ref(), b"row") => {
14631472
let excel_row = row_index(&event, xml.decoder(), last_declared_row)?;
@@ -1573,25 +1582,44 @@ where
15731582
}
15741583

15751584
fn start_cell(event: &BytesStart<'_>, decoder: Decoder, row: &mut RowState) -> Result<CellState> {
1576-
let column = attribute(event, decoder, b"r")?
1577-
.and_then(|reference| parse_column(&reference))
1578-
.unwrap_or(row.next_column);
1585+
let mut column = None;
1586+
let mut style = 0;
1587+
let mut kind = CellKind::Number;
1588+
for attribute in event.attributes().with_checks(false) {
1589+
let attribute = attribute.map_err(|error| stream_error("invalid XML attribute:", error))?;
1590+
if !matches!(
1591+
attribute.key.as_ref().rsplit(|byte| *byte == b':').next(),
1592+
Some(b"r" | b"s" | b"t")
1593+
) {
1594+
continue;
1595+
}
1596+
let value = attribute
1597+
.decode_and_unescape_value(decoder)
1598+
.map_err(|error| stream_error("invalid XML attribute value:", error))?;
1599+
match attribute.key.as_ref().rsplit(|byte| *byte == b':').next() {
1600+
Some(b"r") => column = parse_column(&value),
1601+
Some(b"s") => style = value.parse::<u32>().unwrap_or(0),
1602+
Some(b"t") => {
1603+
kind = match value.as_ref() {
1604+
"s" => CellKind::SharedString,
1605+
"inlineStr" => CellKind::InlineString,
1606+
"b" => CellKind::Boolean,
1607+
"e" => CellKind::Error,
1608+
"str" => CellKind::String,
1609+
"d" => CellKind::IsoDate,
1610+
_ => CellKind::Number,
1611+
};
1612+
}
1613+
_ => {}
1614+
}
1615+
}
1616+
let column = column.unwrap_or(row.next_column);
15791617
row.next_column = column.saturating_add(1);
1580-
let style =
1581-
attribute(event, decoder, b"s")?.and_then(|value| value.parse::<u32>().ok()).unwrap_or(0);
1582-
let kind = match attribute(event, decoder, b"t")?.as_deref() {
1583-
Some("s") => CellKind::SharedString,
1584-
Some("inlineStr") => CellKind::InlineString,
1585-
Some("b") => CellKind::Boolean,
1586-
Some("e") => CellKind::Error,
1587-
Some("str") => CellKind::String,
1588-
Some("d") => CellKind::IsoDate,
1589-
_ => CellKind::Number,
1590-
};
15911618
Ok(CellState {
15921619
column,
15931620
style,
15941621
kind,
1622+
shared_string_index: None,
15951623
value: String::new(),
15961624
inline_text: String::new(),
15971625
formula: None,
@@ -1600,13 +1628,26 @@ fn start_cell(event: &BytesStart<'_>, decoder: Decoder, row: &mut RowState) -> R
16001628
}
16011629

16021630
fn finish_cell(cell: CellState, row: &mut RowState, context: &WorkbookContext) -> Result<()> {
1603-
let CellState { column, style, kind, value, inline_text, formula, capture: _ } = cell;
1631+
let CellState {
1632+
column,
1633+
style,
1634+
kind,
1635+
shared_string_index,
1636+
value,
1637+
inline_text,
1638+
formula,
1639+
capture: _,
1640+
} = cell;
16041641
let cell_style = context.styles.get(style as usize);
16051642
let format = cell_style.map_or(CellFormat::Other, |style| style.format);
16061643
let number_format = cell_style.and_then(|style| style.number_format.clone());
16071644
let data = match kind {
16081645
CellKind::SharedString => {
1609-
if value.is_empty() {
1646+
if let Some(index) = shared_string_index {
1647+
Data::String(context.shared_strings.get(index)?.ok_or_else(|| {
1648+
Error::stream(format!("shared string index {index} is out of range"))
1649+
})?)
1650+
} else if value.is_empty() {
16101651
Data::Empty
16111652
} else {
16121653
let index = value
@@ -1730,14 +1771,26 @@ fn validate_range(options: &ReadOptions) -> Result<()> {
17301771
}
17311772

17321773
fn row_index(event: &BytesStart<'_>, decoder: Decoder, previous: Option<usize>) -> Result<usize> {
1733-
Ok(attribute(event, decoder, b"r")?
1734-
.and_then(|value| value.parse::<usize>().ok())
1735-
.and_then(|value| value.checked_sub(1))
1736-
.unwrap_or_else(|| previous.map_or(0, |row| row.saturating_add(1))))
1774+
for attribute in event.attributes().with_checks(false) {
1775+
let attribute = attribute.map_err(|error| stream_error("invalid XML attribute:", error))?;
1776+
if is_name(attribute.key.as_ref(), b"r") {
1777+
let value = attribute
1778+
.decode_and_unescape_value(decoder)
1779+
.map_err(|error| stream_error("invalid XML attribute value:", error))?;
1780+
if let Some(row) = value.parse::<usize>().ok().and_then(|value| value.checked_sub(1)) {
1781+
return Ok(row);
1782+
}
1783+
break;
1784+
}
1785+
}
1786+
Ok(previous.map_or(0, |row| row.saturating_add(1)))
17371787
}
17381788

17391789
fn append_cell_text(text: &quick_xml::events::BytesText<'_>, cell: &mut CellState) -> Result<()> {
17401790
match cell.capture {
1791+
Some(Capture::Value) if matches!(cell.kind, CellKind::SharedString) => {
1792+
append_shared_string_index(text, &mut cell.shared_string_index)
1793+
}
17411794
Some(Capture::Value) => append_text(text, &mut cell.value),
17421795
Some(Capture::InlineText) => append_text(text, &mut cell.inline_text),
17431796
Some(Capture::Formula) => {
@@ -1747,6 +1800,32 @@ fn append_cell_text(text: &quick_xml::events::BytesText<'_>, cell: &mut CellStat
17471800
}
17481801
}
17491802

1803+
fn append_shared_string_index(
1804+
text: &quick_xml::events::BytesText<'_>,
1805+
target: &mut Option<usize>,
1806+
) -> Result<()> {
1807+
let decoded = text
1808+
.xml10_content()
1809+
.map_err(|error| stream_error("invalid shared string index:", error))?;
1810+
let mut index = target.unwrap_or(0);
1811+
for byte in decoded.bytes() {
1812+
let digit = byte
1813+
.checked_sub(b'0')
1814+
.filter(|digit| *digit <= 9)
1815+
.ok_or_else(|| Error::stream(format!("invalid shared string index '{decoded}'")))?;
1816+
index = index
1817+
.checked_mul(10)
1818+
.and_then(|value| value.checked_add(usize::from(digit)))
1819+
.ok_or_else(|| {
1820+
Error::stream(format!("shared string index '{decoded}' is too large"))
1821+
})?;
1822+
}
1823+
if !decoded.is_empty() {
1824+
*target = Some(index);
1825+
}
1826+
Ok(())
1827+
}
1828+
17501829
fn append_cell_reference(reference: &BytesRef<'_>, cell: &mut CellState) -> Result<()> {
17511830
match cell.capture {
17521831
Some(Capture::Value) => append_reference(reference, &mut cell.value),

0 commit comments

Comments
 (0)