Skip to content

Commit 6a7ea00

Browse files
committed
Add atomic async template exports with .NET parity
1 parent 4078f8f commit 6a7ea00

13 files changed

Lines changed: 558 additions & 15 deletions

‎.github/workflows/rust.yml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,37 @@ jobs:
321321
--filter "FullyQualifiedName~MiniExcelOpenXmlExporterAsyncTests.SaveAsByAsyncEnumerable|FullyQualifiedName~MiniExcelOpenXmlExporterAsyncTests.SaveEmptyAsyncEnumerableTest"
322322
--verbosity minimal
323323
324+
async-template-parity:
325+
strategy:
326+
matrix:
327+
os: [ubuntu-latest, windows-latest]
328+
runs-on: ${{ matrix.os }}
329+
steps:
330+
- uses: actions/checkout@v4
331+
- name: Checkout MiniExcel .NET
332+
uses: actions/checkout@v4
333+
with:
334+
repository: mini-software/MiniExcel
335+
ref: b9a76d7af62142e0e38545b6905b01a06e8d160e
336+
path: dotnet-source
337+
- name: Install Rust 1.85.0
338+
uses: dtolnay/rust-toolchain@1.85.0
339+
- uses: Swatinem/rust-cache@v2
340+
with:
341+
key: async-template-parity-${{ matrix.os }}
342+
- name: Setup .NET
343+
uses: actions/setup-dotnet@v4
344+
with:
345+
dotnet-version: 10.0.x
346+
- name: Run Rust async template tests
347+
run: cargo +1.85.0 test -p miniexcel --test template_async --features async --locked
348+
- name: Run scoped MiniExcel .NET async template tests
349+
run: >-
350+
dotnet test dotnet-source/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj
351+
--framework net10.0
352+
--filter "FullyQualifiedName~MiniExcelTemplateAsyncTests.TemplateAsyncBasicTest|FullyQualifiedName~MiniExcelTemplateAsyncTests.SaveAsByTemplateAsync_TakeCancel_Throws_TaskCanceledException"
353+
--verbosity minimal
354+
324355
msrv:
325356
runs-on: ubuntu-latest
326357
steps:

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,13 @@ MiniExcel::save_as_template(
580580

581581
Scalar placeholders use `{{title}}`. A row containing `{{items.name}}` is repeated once per array item. Exact number, boolean, and null placeholders become native cell values; mixed text becomes an inline string. Missing variables are blank by default and can be rejected with `with_ignore_missing_variables(false)`. Path output refuses existing files unless `with_overwrite_file(true)` is set. `save_as_template_bytes()` supports in-memory templates.
582582

583+
With the optional `async` feature, `save_as_template_async()` and
584+
`save_as_template_async_with_cancellation()` run blocking template ZIP/XML work on a worker thread
585+
and atomically publish the validated path output. Pre-cancellation, rendering errors, dropped
586+
futures, and cancellation before commit preserve an existing destination byte-for-byte or leave a
587+
missing destination absent. This does not make ZIP/filesystem operations asynchronous and retains
588+
the current in-memory template rendering model.
589+
583590
Version 1 does not implement `@group`, `@if`, parametrized sheet cloning, `$=` formula templates, or formula recalculation.
584591

585592
## Important Semantics

‎README.zh-CN.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,12 @@ MiniExcel::save_as_template(
570570

571571
标量占位符使用 `{{title}}`。包含 `{{items.name}}` 的 row 会按数组 item 数量重复。单独的 number、boolean 和 null 占位符会写成原生 cell value;混合文本写成 inline string。缺失变量默认留空,也可用 `with_ignore_missing_variables(false)` 拒绝。路径输出默认拒绝已有文件,设置 `with_overwrite_file(true)` 后才覆盖。内存模板可使用 `save_as_template_bytes()`。
572572

573+
启用可选 `async` feature 后,`save_as_template_async()` 与
574+
`save_as_template_async_with_cancellation()` 会在 worker thread 执行 blocking template
575+
ZIP/XML 工作,并原子发布通过验证的 path output。Pre-cancellation、render error、drop future
576+
以及 commit 前 cancellation 会保留已有 destination 的原始字节,或让缺失 destination 继续
577+
不存在。该 API 不会把 ZIP/filesystem 操作变成 async,并保留当前模板内存渲染模型。
578+
573579
版本 1 尚不支持 `@group`、`@if`、参数化 sheet 克隆、`$=` 公式模板或公式重算。
574580

575581
## 重要语义

‎docs/compatibility.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ The latest `calamine 0.36` and `rust_xlsxwriter 0.97` require Rust 1.88. The MVP
5656
| Async Insert producer | `insert_with_schema_async*()` | Optional `async` feature; bounded producer channel with blocking XLSX work on a dedicated thread |
5757
| Async path query | `query_async*()` / `query_as_async*()` | Optional `async` feature; bounded dynamic/Serde streams, cooperative cancellation, blocking XLSX workers |
5858
| Async explicit-schema export | `save_as_with_schema_async*()` | Optional `async` feature; bounded row producer, atomic destination, cooperative cancellation |
59+
| Async basic template path output | `save_as_template_async*()` | Optional `async` feature; scalar/list renderer, atomic destination, cooperative cancellation |
5960
| Per-sheet visibility | `WriteOptions::with_sheet_visibility()` | Visible, hidden, and very hidden; first visible sheet is active |
6061
| `overwriteFile` | `WriteOptions::with_overwrite_file()` | Defaults to `false`; existing paths require explicit opt-in |
6162
| `FreezeRowCount` / `FreezeColumnCount` | `WriteOptions::with_freeze_row_count()` / `with_freeze_column_count()` | Defaults to one frozen row and zero frozen columns |
@@ -186,6 +187,7 @@ The contract covers only the current common surface: dynamic/typed path queries,
186187
| Async Insert producer | Implemented behind optional feature | Rust cancellation tests; not shared internals |
187188
| Async dynamic/typed path query | Implemented behind optional feature | Rust parity, cancellation, error, and cleanup tests |
188189
| Async explicit-schema path export | Implemented behind optional feature | Rust success, rollback, cancellation, and cleanup tests; pinned .NET async-enumerable baseline |
190+
| Async basic template path output | Implemented behind optional feature | Rust success, rollback, cancellation, and cleanup tests; scoped pinned .NET async template baseline |
189191
| DataReader and broader stream ownership | Deferred | No |
190192
| Append worksheet to existing `.xlsx` workbook | Implemented and atomically committed | Rust tests; shared parity contract not yet extended |
191193
| Strict worksheet replacement | Implemented for plain targets and supported target-owned closures | Rust tests; stale calcChain removed and full recalculation requested |
@@ -200,4 +202,4 @@ This matrix is the coverage claim: Rust does not yet provide complete API parity
200202

201203
## Deferred Work
202204

203-
SQL text parsing, `HAVING`, `ORDER BY`, joins, windows, pivots, disk-spill aggregation, vector indexing, model calls, old Excel formats, advanced template directives and sheet cloning, image authoring, merged-cell APIs, formula calculation/dependency expansion, formula authoring, general styling, async template I/O, inferred/typed async export sources, async borrowed readers/writers, and borrowed XLSX lazy readers require separate design and acceptance milestones. CSV DataReader/DataTable adapters are intentionally replaced by Rust iterators, and a one-call CSV/XLSX converter is not exposed; callers compose query and save APIs. See the [Insert migration guide](insert-v1-migration.md) for supported workflows and deliberate differences.
205+
SQL text parsing, `HAVING`, `ORDER BY`, joins, windows, pivots, disk-spill aggregation, vector indexing, model calls, old Excel formats, advanced template directives and sheet cloning, image authoring, merged-cell APIs, formula calculation/dependency expansion, formula authoring, general styling, async template streams, inferred/typed async export sources, async borrowed readers/writers, and borrowed XLSX lazy readers require separate design and acceptance milestones. CSV DataReader/DataTable adapters are intentionally replaced by Rust iterators, and a one-call CSV/XLSX converter is not exposed; callers compose query and save APIs. See the [Insert migration guide](insert-v1-migration.md) for supported workflows and deliberate differences.

‎docs/compatibility.zh-CN.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Rust MVP 在统一的 `MiniExcel` facade 后实现最小但实用的 MiniExcel
5656
| Async Insert producer | `insert_with_schema_async*()` | 可选 `async` feature;bounded producer channel,XLSX 工作在专用 blocking thread |
5757
| Async path query | `query_async*()` / `query_as_async*()` | 可选 `async` feature;bounded 动态/Serde stream、协作式 cancellation、blocking XLSX worker |
5858
| Async 显式 schema export | `save_as_with_schema_async*()` | 可选 `async` feature;bounded row producer、原子 destination、协作式 cancellation |
59+
| Async 基础 template path output | `save_as_template_async*()` | 可选 `async` feature;scalar/list renderer、原子 destination、协作式 cancellation |
5960
| 每表 visibility | `WriteOptions::with_sheet_visibility()` | visible、hidden、very hidden;第一个 visible sheet 为 active |
6061
| `overwriteFile` | `WriteOptions::with_overwrite_file()` | 默认 `false`;已有路径需要显式允许覆盖 |
6162
| `FreezeRowCount` / `FreezeColumnCount` | `WriteOptions::with_freeze_row_count()` / `with_freeze_column_count()` | 默认冻结一行、零列 |
@@ -186,6 +187,7 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
186187
| Async Insert producer | 已通过可选 feature 实现 | Rust cancellation test;不共享内部行为 |
187188
| Async 动态/类型化 path query | 已通过可选 feature 实现 | Rust parity、cancellation、error 与 cleanup 测试 |
188189
| Async 显式 schema path export | 已通过可选 feature 实现 | Rust success、rollback、cancellation 与 cleanup 测试;固定 .NET async-enumerable 基准 |
190+
| Async 基础 template path output | 已通过可选 feature 实现 | Rust success、rollback、cancellation 与 cleanup 测试;scoped 固定 .NET async template 基准 |
189191
| DataReader 与更广泛的 stream ownership | 延后 | 否 |
190192
| 向现有 `.xlsx` workbook 追加 worksheet | 已实现并原子提交 | Rust 测试;共享 parity contract 尚未扩展 |
191193
| 严格 worksheet replacement | 已支持 plain target 与受支持的 target-owned closure | Rust 测试;删除 stale calcChain 并要求 full recalculation |
@@ -200,4 +202,4 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
200202

201203
## 延后工作
202204

203-
SQL 文本解析、`HAVING`、`ORDER BY`、join、window、pivot、磁盘 spill 聚合、向量索引、模型调用、旧 Excel 格式、高级 template 指令与 sheet 克隆、image authoring、merged-cell API、公式计算/依赖展开、公式编写、通用 style、async template I/O、推断/类型化 async export source、async borrowed reader/writer,以及 borrowed XLSX lazy reader,都需要独立的设计与验收里程碑。CSV DataReader/DataTable adapter 已有意替换为 Rust iterator;当前不提供一步式 CSV/XLSX converter,调用方可组合 query 与 save API。支持工作流与有意保留的差异见 [Insert 迁移说明](insert-v1-migration.zh-CN.md)。
205+
SQL 文本解析、`HAVING`、`ORDER BY`、join、window、pivot、磁盘 spill 聚合、向量索引、模型调用、旧 Excel 格式、高级 template 指令与 sheet 克隆、image authoring、merged-cell API、公式计算/依赖展开、公式编写、通用 style、async template stream、推断/类型化 async export source、async borrowed reader/writer,以及 borrowed XLSX lazy reader,都需要独立的设计与验收里程碑。CSV DataReader/DataTable adapter 已有意替换为 Rust iterator;当前不提供一步式 CSV/XLSX converter,调用方可组合 query 与 save API。支持工作流与有意保留的差异见 [Insert 迁移说明](insert-v1-migration.zh-CN.md)。

‎docs/dotnet-feature-gaps.md‎

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

99
| Project | Revision |
1010
| --- | --- |
11-
| MiniExcel-Rust | working tree based on `693dfbf` (`0.3.0`) |
11+
| MiniExcel-Rust | working tree based on `4078f8f` (`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).
@@ -31,11 +31,11 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
3131
| 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. |
3232
| 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. |
3333
| 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, borrowed async streams, and template streams remain unsupported. |
34-
| Async and cancellation | Partial | Optional runtime-neutral dynamic/Serde path queries, explicit-schema path export, and explicit-schema Insert support bounded streams and cooperative cancellation. Async template operations, inferred/typed async write sources, borrowed async I/O, and progress callbacks remain unsupported. ZIP and filesystem work remains blocking on dedicated workers. |
34+
| Async and cancellation | Partial | Optional runtime-neutral dynamic/Serde path queries, explicit-schema path export, basic template path output, and explicit-schema Insert support cooperative cancellation and atomic publication where applicable. Advanced template streams, inferred/typed async write sources, borrowed async I/O, and progress callbacks remain unsupported. ZIP and filesystem work remains blocking on dedicated workers. |
3535
| 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. |
3636
| Multi-sheet export | Partial | Rust creates ordered visible, hidden, and very-hidden worksheets, but does not yet accept heterogeneous Serde row types in one call. |
3737
| Existing-workbook operations | Implemented | Rust atomically appends, strictly replaces, renames, changes visibility, reorders, and performs .NET-style source-workbook copy-and-add while preserving unrelated package parts and worksheet identity. Rename preserves formula text; visibility rejects hiding the final visible sheet; reorder remaps active/view/local-name indices; copy-and-add preserves the source and atomically publishes a separate destination. |
38-
| Templates | Partial | Rust fills path/byte templates with scalar values and single-row arrays while preserving package parts. Streams, grouping, conditions, parametrized sheets, `$=` formulas, formula-reference updates, and calculation-chain handling remain unsupported. |
38+
| Templates | Partial | Rust fills path/byte templates with scalar values and single-row arrays while preserving package parts; path output also has a cancellable async wrapper. Streams, grouping, conditions, parametrized sheets, `$=` formulas, formula-reference updates, and calculation-chain handling remain unsupported. |
3939
| Pictures and merge processing | Missing | Add anchored pictures and merge adjacent identical cells through the templater surface. Structured reads do not provide authoring parity. |
4040
| CSV | Implemented | Dynamic/Serde path, byte, and borrowed query/save APIs; column discovery; inferred/explicit-schema append; delimiter/newline/encoding/BOM/null/quoting configuration; and `query-csv` CLI. DataReader/DataTable are replaced by Rust iterators. Async/progress APIs and a one-call CSV/XLSX converter are not exposed. |
4141
| Comments and notes | Implemented | Path/bytes/borrowed APIs return threaded roots, replies, unresolved person IDs, people/provider/user IDs, resolution state, typed timestamps, and legacy notes. |
@@ -53,6 +53,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
5353
| Public read/write boundary | `miniexcel/src/facade.rs`, `miniexcel/src/options.rs` | `src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs`, `OpenXmlExporter.cs` |
5454
| Async query | `MiniExcel::query_async*` and `query_as_async*`; Rust focused parity/cancellation/error/cleanup tests | `OpenXmlImporter.QueryAsync`; `MiniExcelOpenXmlImporterAsyncTests` |
5555
| Async export | `MiniExcel::save_as_with_schema_async*`; Rust focused rollback/cancellation/cleanup tests | `OpenXmlExporter.ExportAsync`; async-enumerable exporter tests |
56+
| Async template | `MiniExcel::save_as_template_async*`; Rust focused rollback/cancellation/cleanup tests | `OpenXmlTemplater.SaveAsByTemplateAsync`; scoped basic/cancellation tests |
5657
| Tables | `MiniExcel::query_table*`; Rust focused tests use the exact `TestQueryTable.xlsx` fixture (SHA-256 `04F719BF9F9E99D9B437A8FB32F8111FD92580A1D29ACAD10B6ED128C0564501`) | `OpenXmlImporter.QueryTableAsync`; `tests/MiniExcel.OpenXml.Tests/Tables/` |
5758
| Comments | `MiniExcel::get_comments*`; Rust focused tests use `TestCommentsAndNotes.xlsx` (SHA-256 `3A855CE896ED62DC27C91797432DD89EE081F07CD03AB05BF1B0CD745543A3FC`) | `OpenXmlImporter.RetrieveCommentsAsync`; `tests/MiniExcel.OpenXml.Tests/Comments/` |
5859
| 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/` |
@@ -67,7 +68,7 @@ The .NET APIs marked with `Async` also have generated synchronous counterparts t
6768

6869
## Suggested Implementation Order
6970

70-
1. **Async template and broader export APIs**: extend runtime-neutral cancellation to templates and typed/inferred producers without presenting blocking ZIP work as async I/O.
71+
1. **Broader async export/stream APIs**: extend runtime-neutral cancellation to typed/inferred producers and borrowed streams without presenting blocking ZIP work as async I/O.
7172
2. **Advanced templates and Fluent Mapping**: add grouped/conditional templates, parametrized sheets, and mapping through separate compatibility milestones.
7273
3. **Selected-sheet cloning**: not part of .NET `CopyAndAddSheet`; add only for a concrete Rust use case with a relationship-closure cloning contract.
7374

0 commit comments

Comments
 (0)