Skip to content

Commit 23bba2d

Browse files
committed
Add named OpenXML table queries with .NET
1 parent 1e31672 commit 23bba2d

13 files changed

Lines changed: 1034 additions & 16 deletions

File tree

‎.github/workflows/rust.yml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,37 @@ jobs:
101101
--filter "FullyQualifiedName~RustParityContractTests"
102102
--verbosity minimal
103103
104+
named-table-parity:
105+
strategy:
106+
matrix:
107+
os: [ubuntu-latest, windows-latest]
108+
runs-on: ${{ matrix.os }}
109+
steps:
110+
- uses: actions/checkout@v4
111+
- name: Checkout MiniExcel .NET
112+
uses: actions/checkout@v4
113+
with:
114+
repository: mini-software/MiniExcel
115+
ref: b9a76d7af62142e0e38545b6905b01a06e8d160e
116+
path: dotnet-source
117+
- name: Install Rust 1.85.0
118+
uses: dtolnay/rust-toolchain@1.85.0
119+
- uses: Swatinem/rust-cache@v2
120+
with:
121+
key: named-table-parity-${{ matrix.os }}
122+
- name: Setup .NET
123+
uses: actions/setup-dotnet@v4
124+
with:
125+
dotnet-version: 10.0.x
126+
- name: Run Rust named-table tests
127+
run: cargo +1.85.0 test -p miniexcel --test query_table --locked
128+
- name: Run MiniExcel .NET named-table tests
129+
run: >-
130+
dotnet test dotnet-source/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj
131+
--framework net10.0
132+
--filter "FullyQualifiedName~MiniExcelOpenXmlTableTests"
133+
--verbosity minimal
134+
104135
msrv:
105136
runs-on: ubuntu-latest
106137
steps:

‎README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,22 @@ Without headers, dynamic keys use the actual Excel column names such as `A`, `B`
206206

207207
Merged cells retain only their physical top-left value by default. Use `ReadOptions::with_fill_merged_cells(true)` to project that value across the merged range for dynamic, typed, and byte queries. Structured queries remain sparse and expose only physically stored cells.
208208

209+
### Named Table Query
210+
211+
Query an OpenXML table by its metadata name without reading cells outside its declared range:
212+
213+
```rust
214+
let rows = MiniExcel::query_table("book.xlsx", "SalesTable", Some("Data"))?
215+
.collect::<miniexcel::Result<Vec<_>>>()?;
216+
```
217+
218+
`query_table_as::<T>()` provides Serde mapping, `query_table_bytes()` supports in-memory XLSX data,
219+
and `visit_table_rows*_from_reader()` keeps borrowed readers open. Table names are matched
220+
case-insensitively against the table `name` (not `displayName`). When no sheet is provided, only
221+
the first worksheet is searched. Column names come from table metadata, the physical header row is
222+
skipped unless `headerRowCount="0"`, and the complete declared range is returned, including totals
223+
rows. Path queries retain the existing bounded-memory two-pass worksheet pipeline.
224+
209225
## Typed Reading
210226

211227
```rust

‎README.zh-CN.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,21 @@ for row in MiniExcel::query_with_options("book.xlsx", &options)? {
206206

207207
合并单元格默认只保留物理存储的左上角值。使用 `ReadOptions::with_fill_merged_cells(true)` 可在动态、类型化和 byte query 中将该值投影到整个合并范围。Structured query 仍保持稀疏,只暴露物理存储的 cell。
208208

209+
### 命名 Table Query
210+
211+
可按 OpenXML table metadata name 查询,并且不会读取声明 range 外的 cell:
212+
213+
```rust
214+
let rows = MiniExcel::query_table("book.xlsx", "SalesTable", Some("Data"))?
215+
.collect::<miniexcel::Result<Vec<_>>>()?;
216+
```
217+
218+
`query_table_as::<T>()` 提供 Serde mapping,`query_table_bytes()` 支持内存 XLSX,
219+
`visit_table_rows*_from_reader()` 会保持 borrowed reader open。Table name 会按 table
220+
`name`(不是 `displayName`)进行大小写不敏感匹配。未指定 sheet 时只搜索第一张 worksheet。
221+
Column name 来自 table metadata;除非 `headerRowCount="0"`,否则会跳过物理 header row;
222+
返回完整声明 range,包括 totals row。Path query 继续使用既有有界内存两遍 worksheet pipeline。
223+
209224
## 类型化读取
210225

211226
```rust

‎docs/compatibility.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The latest `calamine 0.36` and `rust_xlsxwriter 0.97` require Rust 1.88. The MVP
4242
| `GetSheetInformations` | `MiniExcel::get_sheet_info()` | Includes OOXML ID, order, name, type, visibility, and active state |
4343
| `GetSheetDimensions` | `MiniExcel::get_sheet_dimensions()` | Returns used ranges in workbook order with 1-based indices |
4444
| `GetColumns` | `MiniExcel::get_columns()` | Returns selected dynamic keys or an empty vector |
45+
| `QueryTable` | `query_table()` / `query_table_as()` / byte and borrowed-reader variants | Case-insensitive table-name lookup, metadata headers, and inclusive table bounds |
4546
| `startCell` | `ReadOptions::with_start_cell()` | A1 start coordinate |
4647
| `IgnoreEmptyRows` | `ReadOptions::with_ignore_empty_rows()` | Defaults to `false` for MiniExcel compatibility |
4748
| `FillMergedCells` | `ReadOptions::with_fill_merged_cells()` | Defaults to `false`; applies to dynamic, typed, and byte queries |
@@ -170,6 +171,7 @@ The contract covers only the current common surface: dynamic/typed path queries,
170171
| `GetSheetNames` and `GetColumns` | Implemented | Yes |
171172
| `GetSheetInformations` ID/index/name/type/visibility/active | Implemented | Rust tests against .NET fixtures |
172173
| `GetSheetDimensions` | Implemented | Rust tests against .NET fixtures |
174+
| Named OpenXML `QueryTable` | Implemented | Rust and .NET focused tests against `TestQueryTable.xlsx` |
173175
| New-workbook `SaveAs`, including multiple sheets | Implemented and roundtrip-tested | Not yet |
174176
| Basic `SaveAsTemplate` scalar/list fill | Implemented and roundtrip-tested | Not yet |
175177
| Byte-array query/write for WASM | Implemented | Rust/browser tests |

‎docs/compatibility.zh-CN.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Rust MVP 在统一的 `MiniExcel` facade 后实现最小但实用的 MiniExcel
4242
| `GetSheetInformations` | `MiniExcel::get_sheet_info()` | 包含 OOXML ID、顺序、名称、类型、visibility 和 active 状态 |
4343
| `GetSheetDimensions` | `MiniExcel::get_sheet_dimensions()` | 按 workbook 顺序返回使用范围,index 从 1 开始 |
4444
| `GetColumns` | `MiniExcel::get_columns()` | 返回选中的动态 key,或空 vector |
45+
| `QueryTable` | `query_table()` / `query_table_as()` / byte 和 borrowed-reader variants | 大小写不敏感 table-name lookup、metadata header 与包含端点的 table bounds |
4546
| `startCell` | `ReadOptions::with_start_cell()` | A1 起始坐标 |
4647
| `IgnoreEmptyRows` | `ReadOptions::with_ignore_empty_rows()` | 为兼容 MiniExcel,默认值为 `false` |
4748
| `FillMergedCells` | `ReadOptions::with_fill_merged_cells()` | 默认 `false`;适用于动态、类型化和 byte query |
@@ -170,6 +171,7 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
170171
| `GetSheetNames` 和 `GetColumns` | 已实现 | 是 |
171172
| `GetSheetInformations` ID/index/name/type/visibility/active | 已实现 | Rust 使用 .NET fixture 测试 |
172173
| `GetSheetDimensions` | 已实现 | Rust 使用 .NET fixture 测试 |
174+
| 命名 OpenXML `QueryTable` | 已实现 | Rust/.NET 使用 `TestQueryTable.xlsx` 的 focused test |
173175
| 新 workbook `SaveAs`(含多工作表) | 已实现并完成 roundtrip 测试 | 尚未 |
174176
| 基础 `SaveAsTemplate` 标量/列表填充 | 已实现并完成 roundtrip 测试 | 尚未 |
175177
| 用于 WASM 的字节数组 query/write | 已实现 | Rust/browser 测试 |

‎docs/dotnet-feature-gaps.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ This report compares observable public capabilities in the local checkouts below
88

99
| Project | Revision |
1010
| --- | --- |
11-
| MiniExcel-Rust | `8a76d1af50c039967875511e2e3ca7c746241e51` (`0.3.0`) |
11+
| MiniExcel-Rust | `1e3167222142a86710e05269f9fcbbba11185d22` (`0.3.0`) |
1212
| MiniExcel .NET | `b9a76d7af62142e0e38545b6905b01a06e8d160e` |
1313

1414
The comparison uses the .NET public APIs, their controlling implementations, and focused tests under the sibling `../MiniExcel` checkout. Rust status is based on the public `MiniExcel` facade, options, integration tests, and [compatibility boundary](compatibility.md).
1515

1616
Status meanings:
1717

18+
- **Implemented**: the useful observable XLSX behavior is covered by a Rust-native API and focused cross-project evidence.
1819
- **Partial**: Rust covers the core scenario but not the full observable .NET behavior.
1920
- **Missing**: no equivalent public Rust capability exists.
2021
- **Different by design**: Rust provides a native alternative, but it is not API or behavior parity.
@@ -27,8 +28,8 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
2728

2829
| Area | Status | Capability available in .NET but not fully implemented in Rust |
2930
| --- | --- | --- |
30-
| Named tables | Missing | Query an OpenXML table by table name (`QueryTable`), including table-specific headers and bounds. |
31-
| DataReader and DataTable | Missing | `IDataReader`/async reader access, schema tables, typed getters, `NextResult` sheet traversal, and `DataTable` materialization. |
31+
| Named tables | Implemented | Dynamic/typed path queries, byte queries, and borrowed-reader visitors use table metadata headers and bounds with case-insensitive table-name matching. |
32+
| DataReader and DataTable | Different by design | Rust exposes iterators and borrowed visitors instead of .NET tabular interfaces. A Rust-native Arrow/record-batch adapter is deferred until a concrete integration requires it and does not block parity completion. |
3233
| Caller-owned streams | Partial | Borrowed synchronous dynamic/typed/structured visitors, metadata reads, dynamic/schema/typed/multi-sheet writers, and separate reader-to-writer Insert are implemented with leave-open semantics. Borrowed lazy iterators, async streams, and template streams remain unsupported. |
3334
| Async and cancellation | Partial | Optional runtime-neutral async row production and cancellation are implemented for explicit-schema path Insert. Async query/export/template operations, typed async sources, and progress callbacks remain unsupported. ZIP and filesystem work remains blocking on a dedicated worker. |
3435
| General save inputs | Partial | Export from general objects/enumerables, dictionaries, `DataTable`, `IDataReader`, and async enumerables, with progress. Rust accepts dynamic or same-type Serde slices and reports per-sheet row counts. |
@@ -50,8 +51,8 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
5051
| Area | Rust evidence | .NET evidence in `../MiniExcel` |
5152
| --- | --- | --- |
5253
| Public read/write boundary | `miniexcel/src/facade.rs`, `miniexcel/src/options.rs` | `src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs`, `OpenXmlExporter.cs` |
53-
| Tables | No public table API | `OpenXmlImporter.QueryTableAsync`; `tests/MiniExcel.OpenXml.Tests/Tables/` |
54-
| DataReader/DataTable | No public tabular adapter | `OpenXmlImporter.GetDataReader`, `GetAsyncDataReader`, `QueryAsDataTableAsync`; `tests/MiniExcel.OpenXml.Tests/DataReader/` |
54+
| Tables | `MiniExcel::query_table*`; Rust focused tests use the exact `TestQueryTable.xlsx` fixture (SHA-256 `04F719BF9F9E99D9B437A8FB32F8111FD92580A1D29ACAD10B6ED128C0564501`) | `OpenXmlImporter.QueryTableAsync`; `tests/MiniExcel.OpenXml.Tests/Tables/` |
55+
| DataReader/DataTable | Rust iterators and borrowed visitors are the native abstraction; no literal .NET tabular adapter is planned | `OpenXmlImporter.GetDataReader`, `GetAsyncDataReader`, `QueryAsDataTableAsync`; `tests/MiniExcel.OpenXml.Tests/DataReader/` |
5556
| Multi-sheet and workbook edits | Writer creates multiple sheets; existing workbooks support append and strict replacement with package preservation and bounded explicit-schema producers | `OpenXmlExporter.InsertSheetAsync`, `CopyAndAddSheetAsync`, `AlterSheetAsync`; `tests/MiniExcel.OpenXml.Tests/MultipleSheets/` and `AlterSheets/` |
5657
| Templates/pictures/merges | Basic template fill implemented; advanced directives and authoring remain deferred | `src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs`; `tests/MiniExcel.OpenXml.Tests/Templates/` |
5758
| CSV/conversion | XLSX-only core | `src/MiniExcel.Csv/Api/`; `src/MiniExcel/MiniExcelConverter.cs`; `tests/MiniExcel.Csv.Tests/` |
@@ -63,13 +64,13 @@ The .NET APIs marked with `Async` also have generated synchronous counterparts t
6364

6465
## Suggested Implementation Order
6566

66-
1. **Named-table query and comments**: focused OpenXML read features with clear fixtures and public result models.
67+
1. **Comments and notes**: focused OpenXML read features with clear public result models.
6768
2. **CSV provider**: keep a separate format boundary rather than conditionals inside the XLSX parser.
6869
3. **Async query/export/template APIs**: extend runtime-neutral producer/cancellation patterns without presenting blocking ZIP work as async I/O.
6970
4. **Advanced templates and Fluent Mapping**: add grouped/conditional templates, parametrized sheets, and mapping through separate compatibility milestones.
7071
5. **Remaining workbook edits**: copy/add, rename, reorder, and standalone visibility mutation require their own preservation contracts.
7172

72-
DataReader/DataTable are .NET ecosystem abstractions and should not be ported literally. A Rust-native record-batch or tabular adapter is appropriate only when a concrete integration requires it.
73+
DataReader/DataTable are .NET ecosystem abstractions and are intentionally not literal Rust parity requirements. A Rust-native record-batch or tabular adapter is appropriate only when a concrete integration requires it.
7374

7475
## Not Counted as Gaps
7576

‎docs/dotnet-feature-gaps.zh-CN.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88

99
| 项目 | 版本基线 |
1010
| --- | --- |
11-
| MiniExcel-Rust | `8a76d1af50c039967875511e2e3ca7c746241e51`(`0.3.0`) |
11+
| MiniExcel-Rust | `1e3167222142a86710e05269f9fcbbba11185d22`(`0.3.0`) |
1212
| MiniExcel .NET | `b9a76d7af62142e0e38545b6905b01a06e8d160e` |
1313

1414
比对依据包括同级目录 `../MiniExcel` 中的 .NET 公开 API、控制实现和聚焦测试,以及 Rust 的公开 `MiniExcel` 门面、选项、集成测试和[兼容性边界](compatibility.zh-CN.md)。
1515

1616
状态定义:
1717

18+
- **已实现**:有实际价值的可观察 XLSX 行为已由 Rust-native API 与跨项目 focused evidence 覆盖。
1819
- **部分实现**:Rust 已覆盖核心场景,但尚未覆盖 .NET 的完整可观察行为。
1920
- **未实现**:Rust 当前没有对应的公开能力。
2021
- **设计不同**:Rust 提供了原生替代方案,但不构成 API 或行为等价。
@@ -27,8 +28,8 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
2728

2829
| 领域 | 状态 | .NET 已有而 Rust 尚未完整实现的能力 |
2930
| --- | --- | --- |
30-
| 命名表格 | 未实现 | 按表格名称查询 OpenXML Table(`QueryTable`),并遵循表格自身的表头与范围。 |
31-
| DataReader 与 DataTable | 未实现 | `IDataReader`/异步 reader、架构表、强类型 getter、通过 `NextResult` 遍历工作表,以及物化为 `DataTable`。 |
31+
| 命名表格 | 已实现 | 动态/类型化 path query、byte query 与 borrowed-reader visitor 使用 table metadata header 和 bounds,并按 table name 大小写不敏感匹配。 |
32+
| DataReader 与 DataTable | 设计不同 | Rust 使用 iterator 与 borrowed visitor,不复制 .NET tabular interface。只有具体集成需要时才考虑 Rust-native Arrow/record-batch adapter;此项不阻塞 parity 完成。 |
3233
| 调用方提供的流 | 部分实现 | 已实现借用的同步动态/类型化/structured visitor、metadata 读取、动态/schema/类型化/多表 writer,以及独立 reader-to-writer Insert,并保持 leave-open。借用 lazy iterator、async stream 和 template stream 仍不支持。 |
3334
| 异步与取消 | 部分实现 | 显式 schema path Insert 已支持可选、runtime-neutral 的 async row producer 与 cancellation。Async query/export/template、类型化 async source 和 progress callback 仍不支持;ZIP 与 filesystem 工作仍在专用 blocking worker 上执行。 |
3435
| 通用保存输入 | 部分实现 | 从普通对象/可枚举对象、字典、`DataTable`、`IDataReader` 和异步枚举导出,并报告进度。Rust 接受动态行或同类型 Serde 切片,并返回每张工作表的行数。 |
@@ -50,8 +51,8 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
5051
| 领域 | Rust 证据 | `../MiniExcel` 中的 .NET 证据 |
5152
| --- | --- | --- |
5253
| 公开读写边界 | `miniexcel/src/facade.rs`、`miniexcel/src/options.rs` | `src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs`、`OpenXmlExporter.cs` |
53-
| 表格 | 无公开表格 API | `OpenXmlImporter.QueryTableAsync`;`tests/MiniExcel.OpenXml.Tests/Tables/` |
54-
| DataReader/DataTable | 无公开表格适配器 | `OpenXmlImporter.GetDataReader`、`GetAsyncDataReader`、`QueryAsDataTableAsync`;`tests/MiniExcel.OpenXml.Tests/DataReader/` |
54+
| 表格 | `MiniExcel::query_table*`;Rust focused test 使用完全相同的 `TestQueryTable.xlsx` fixture(SHA-256 `04F719BF9F9E99D9B437A8FB32F8111FD92580A1D29ACAD10B6ED128C0564501`) | `OpenXmlImporter.QueryTableAsync`;`tests/MiniExcel.OpenXml.Tests/Tables/` |
55+
| DataReader/DataTable | Rust iterator 与 borrowed visitor 是原生抽象;不计划逐字复制 .NET tabular adapter | `OpenXmlImporter.GetDataReader`、`GetAsyncDataReader`、`QueryAsDataTableAsync`;`tests/MiniExcel.OpenXml.Tests/DataReader/` |
5556
| 多工作表与工作簿修改 | Writer 可创建多个工作表;现有 workbook 支持 append 与严格 replacement,并保留 package,显式 schema producer 采用有界资源处理 | `OpenXmlExporter.InsertSheetAsync`、`CopyAndAddSheetAsync`、`AlterSheetAsync`;`MultipleSheets/` 与 `AlterSheets/` 测试 |
5657
| 模板/图片/合并 | 已实现基础模板填充;高级指令与 authoring 仍延期 | `src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs`;`tests/MiniExcel.OpenXml.Tests/Templates/` |
5758
| CSV/转换 | 核心仅支持 XLSX | `src/MiniExcel.Csv/Api/`;`src/MiniExcel/MiniExcelConverter.cs`;`tests/MiniExcel.Csv.Tests/` |
@@ -63,13 +64,13 @@ Rust 已支持动态及 Serde 强类型 XLSX 路径查询、闭区间 A1 范围
6364

6465
## 建议实现顺序
6566

66-
1. **命名表格查询与批注读取**:范围聚焦的 OpenXML 读取功能,容易建立明确 fixture 和公开结果模型。
67+
1. **Comments 与 notes 读取**:范围聚焦的 OpenXML read feature,需要明确 public result model。
6768
2. **CSV provider**:保持独立格式边界,不在 XLSX parser 内堆叠条件分支。
6869
3. **Async query/export/template API**:延续 runtime-neutral producer/cancellation 模式,不把 blocking ZIP 工作描述成 async I/O。
6970
4. **高级模板与 Fluent Mapping**:通过独立兼容里程碑增加分组/条件模板、参数化 sheet 和 mapping。
7071
5. **剩余 workbook edit**:copy/add、rename、reorder 和独立 visibility mutation 需要各自的 preservation contract。
7172

72-
DataReader/DataTable 属于 .NET 生态抽象,不建议逐字移植。只有出现明确集成需求时,才应设计 Rust 原生的 record-batch 或表格适配器。
73+
DataReader/DataTable 属于 .NET 生态抽象,明确不作为逐字 Rust parity 要求。只有出现具体集成需求时,才应设计 Rust-native record-batch 或 table adapter。
7374

7475
## 不计为缺口
7576

‎miniexcel/src/error.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ enum ErrorKind {
3030
#[error("worksheet '{0}' was not found")]
3131
SheetNotFound(String),
3232

33+
#[error("table '{0}' was not found")]
34+
TableNotFound(String),
35+
36+
#[error("invalid table '{name}': {reason}")]
37+
InvalidTable { name: String, reason: String },
38+
3339
#[error("the workbook does not contain any worksheets")]
3440
NoWorksheets,
3541

@@ -129,6 +135,14 @@ impl Error {
129135
ErrorKind::SheetNotFound(sheet_name.into()).into()
130136
}
131137

138+
pub(crate) fn table_not_found(table_name: impl Into<String>) -> Self {
139+
ErrorKind::TableNotFound(table_name.into()).into()
140+
}
141+
142+
pub(crate) fn invalid_table(name: impl Into<String>, reason: impl Into<String>) -> Self {
143+
ErrorKind::InvalidTable { name: name.into(), reason: reason.into() }.into()
144+
}
145+
132146
pub(crate) fn no_worksheets() -> Self {
133147
ErrorKind::NoWorksheets.into()
134148
}

0 commit comments

Comments
 (0)