Skip to content

Commit 693dfbf

Browse files
committed
Add atomic workbook copy-and-add with .NET parity
1 parent db95441 commit 693dfbf

16 files changed

Lines changed: 736 additions & 18 deletions

‎.github/workflows/rust.yml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,37 @@ jobs:
259259
--filter "FullyQualifiedName~MiniExcelAlterSheetTests|FullyQualifiedName~MiniExcelAlterSheetsTestAsync"
260260
--verbosity minimal
261261
262+
copy-and-add-parity:
263+
strategy:
264+
matrix:
265+
os: [ubuntu-latest, windows-latest]
266+
runs-on: ${{ matrix.os }}
267+
steps:
268+
- uses: actions/checkout@v4
269+
- name: Checkout MiniExcel .NET
270+
uses: actions/checkout@v4
271+
with:
272+
repository: mini-software/MiniExcel
273+
ref: b9a76d7af62142e0e38545b6905b01a06e8d160e
274+
path: dotnet-source
275+
- name: Install Rust 1.85.0
276+
uses: dtolnay/rust-toolchain@1.85.0
277+
- uses: Swatinem/rust-cache@v2
278+
with:
279+
key: copy-and-add-parity-${{ matrix.os }}
280+
- name: Setup .NET
281+
uses: actions/setup-dotnet@v4
282+
with:
283+
dotnet-version: 10.0.x
284+
- name: Run Rust copy-and-add tests
285+
run: cargo +1.85.0 test -p miniexcel --test copy_and_add_sheet --locked
286+
- name: Run MiniExcel .NET copy-and-add baseline tests
287+
run: >-
288+
dotnet test dotnet-source/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj
289+
--framework net10.0
290+
--filter "FullyQualifiedName~MiniExcelOpenXmlExporterTests.CopyAndInsertSheetTest|FullyQualifiedName~MiniExcelOpenXmlExporterAsyncTests.CopyAndInsertSheetTest"
291+
--verbosity minimal
292+
262293
msrv:
263294
runs-on: ubuntu-latest
264295
steps:

‎Cargo.lock‎

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ rust_xlsxwriter = { version = "0.96.0", features = ["chrono", "serde"] }
3131
serde = { version = "1.0.228", features = ["derive"] }
3232
serde_json = "1.0.145"
3333
sha2 = "0.10.9"
34+
same-file = "1.0.6"
3435
tempfile = "3.23.0"
3536
thiserror = "2.0.17"
3637
uuid = "1.18.1"

‎README.md‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,26 @@ Negative indices clamp to the first position and oversized indices clamp to the
456456
formula text, visibility, and worksheet IDs remain unchanged. Moving to the current effective
457457
index is a byte-for-byte no-op.
458458

459+
Like .NET `CopyAndAddSheet`, Rust can copy a complete source workbook to a separate destination
460+
and generate or replace one worksheet in the copied package:
461+
462+
```rust
463+
let count = MiniExcel::copy_and_add_sheet(
464+
"source.xlsx",
465+
"destination.xlsx",
466+
&rows,
467+
&InsertOptions::new().with_sheet_name("Added"),
468+
)?;
469+
```
470+
471+
Dynamic, explicit-schema one-pass, and Serde variants are available. The source is never modified;
472+
source aliases are rejected, and the destination is atomically published only after package
473+
validation. Existing destinations are rejected unless `with_overwrite_file(true)` is set.
474+
`ExistingSheetPolicy` and `TargetRelationshipPolicy` retain their Insert meanings. Unlike .NET's
475+
shallow package regeneration, Rust raw-copies unrelated parts and preserves workbook identities,
476+
relationships, defined names, formulas, tables, drawings, comments, pivots, and external links.
477+
This API copies the whole workbook and adds data; it does not clone an arbitrary selected worksheet.
478+
459479
Enable the optional `async` feature to feed an existing-workbook Insert from a runtime-neutral
460480
`Stream<Item = miniexcel::Result<DynamicRow>>`:
461481

@@ -554,10 +574,10 @@ Version 1 does not implement `@group`, `@if`, parametrized sheet cloning, `$=` f
554574
- Grouped analytics retain state proportional to distinct groups and stop at `max_groups`.
555575
- RAG exports never recalculate formulas and reject hidden sheets unless explicitly allowed.
556576
- Synchronous streaming queries use one worker thread per active query. Optional async query and Insert APIs use bounded channels around blocking XLSX workers; ZIP/XML/filesystem work is not async I/O.
557-
- Save creates new workbooks and refuses existing target paths by default. `MiniExcel::insert*()` atomically appends or strictly replaces a worksheet in an existing `.xlsx` path, or creates a workbook when the path is missing. `rename_sheet()`, `set_sheet_visibility()`, and `reorder_sheet()` atomically change existing workbook sheet metadata.
577+
- Save creates new workbooks and refuses existing target paths by default. `MiniExcel::insert*()` atomically appends or strictly replaces a worksheet in an existing `.xlsx` path, or creates a workbook when the path is missing. `copy_and_add_sheet*()` creates a validated source-derived destination. `rename_sheet()`, `set_sheet_visibility()`, and `reorder_sheet()` atomically change existing workbook sheet metadata.
558578

559579
## Not Supported
560580

561-
`.xls`, `.xlsb`, `.ods`, advanced template directives, macros, images, merged-cell operations, formula authoring, a general style system, and arbitrary worksheet copying are not currently supported.
581+
`.xls`, `.xlsb`, `.ods`, advanced template directives, macros, image authoring, merged-cell operations, formula authoring, a general style system, and arbitrary selected-worksheet cloning are not currently supported.
562582

563583
See the [compatibility matrix](docs/compatibility.md) for the current support scope and the [MiniExcel v1 Insert migration guide](docs/insert-v1-migration.md) for deliberate differences.

‎README.zh-CN.md‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,26 @@ MiniExcel::reorder_sheet("book.xlsx", "Archive", 0)?;
448448
仍归属于原 worksheet。Relationship、formula 文本、visibility 和 worksheet ID 保持不变;
449449
移动到当前有效 index 是字节级 no-op。
450450

451+
与 .NET `CopyAndAddSheet` 一样,Rust 可将完整 source workbook 复制到独立 destination,
452+
并在复制后的 package 中生成或 replace 一张 worksheet:
453+
454+
```rust
455+
let count = MiniExcel::copy_and_add_sheet(
456+
"source.xlsx",
457+
"destination.xlsx",
458+
&rows,
459+
&InsertOptions::new().with_sheet_name("Added"),
460+
)?;
461+
```
462+
463+
提供 dynamic、显式 schema one-pass 和 Serde 变体。Source 永不修改;source alias 会被拒绝,
464+
destination 只有在 package validation 后才原子发布。默认拒绝已有 destination,设置
465+
`with_overwrite_file(true)` 后才允许替换。`ExistingSheetPolicy` 与
466+
`TargetRelationshipPolicy` 保持 Insert 语义。不同于 .NET 的 shallow package regeneration,
467+
Rust 会 raw-copy 无关 part,并保留 workbook identity、relationship、defined name、formula、
468+
table、drawing、comment、pivot 与 external link。该 API 复制整个 workbook 并添加数据,
469+
并不是克隆任意选定 worksheet。
470+
451471
启用可选 `async` feature 后,可通过 runtime-neutral
452472
`Stream<Item = miniexcel::Result<DynamicRow>>` 为现有 workbook 的 Insert 提供 row:
453473

@@ -545,10 +565,10 @@ MiniExcel::save_as_template(
545565
- 分组分析保留与不同 group 数量成比例的状态,并在 `max_groups` 停止。
546566
- RAG 导出不会重新计算公式,hidden sheet 未显式允许时会拒绝处理。
547567
- 同步流式 query 每个活动 query 使用一个 worker thread。可选 async query/Insert API 通过 bounded channel 包装 blocking XLSX worker;ZIP/XML/filesystem 工作并不是 async I/O。
548-
- Save 会创建新工作簿,并默认拒绝已有目标路径。`MiniExcel::insert*()` 会向现有 `.xlsx` path 原子 append 或严格 replace worksheet;路径不存在时则创建 workbook。`rename_sheet()`、`set_sheet_visibility()` 和 `reorder_sheet()` 会原子修改现有 workbook 的 sheet metadata。
568+
- Save 会创建新工作簿,并默认拒绝已有目标路径。`MiniExcel::insert*()` 会向现有 `.xlsx` path 原子 append 或严格 replace worksheet;路径不存在时则创建 workbook。`copy_and_add_sheet*()` 创建经过验证、源自 source 的 destination。`rename_sheet()`、`set_sheet_visibility()` 和 `reorder_sheet()` 会原子修改现有 workbook 的 sheet metadata。
549569

550570
## 暂不支持
551571

552-
目前不支持 `.xls`、`.xlsb`、`.ods`、高级模板指令、宏、图片、合并单元格操作、公式写入、通用样式系统,以及任意 worksheet copy。
572+
目前不支持 `.xls`、`.xlsb`、`.ods`、高级模板指令、宏、图片写入、合并单元格操作、公式写入、通用样式系统,以及克隆任意选定 worksheet。
553573

554574
当前支持范围请查看[兼容性矩阵](docs/compatibility.zh-CN.md);有意保留的差异见 [MiniExcel v1 Insert 迁移说明](docs/insert-v1-migration.zh-CN.md)。

‎docs/compatibility.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ The contract covers only the current common surface: dynamic/typed path queries,
191191
| Atomic worksheet visibility mutation | Implemented for existing `.xlsx` paths | Rust invariant/rollback tests; pinned .NET `AlterSheet` baseline |
192192
| Atomic worksheet reorder | Implemented for existing `.xlsx` paths | Rust positional-reference/rollback tests; pinned .NET `AlterSheet` baseline |
193193
| Legacy `.xls`, `.xlsb`, and `.ods` formats | Deferred | No |
194-
| Worksheet copy-and-add | Deferred | No |
194+
| Atomic workbook copy-and-add | Implemented for dynamic/schema/Serde path APIs | Rust source/destination rollback tests; pinned .NET `CopyAndAddSheet` baseline |
195195
| Advanced templates, pictures, and merges | Deferred | No |
196196

197197
This matrix is the coverage claim: Rust does not yet provide complete API parity with the current .NET packages.

‎docs/compatibility.zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ Rust workflow 会在 Linux 和 Windows 上运行 Rust 契约。其 .NET parity j
191191
| 原子 worksheet visibility mutation | 已支持现有 `.xlsx` path | Rust invariant/rollback 测试;固定 .NET `AlterSheet` 基准 |
192192
| 原子 worksheet reorder | 已支持现有 `.xlsx` path | Rust positional-reference/rollback 测试;固定 .NET `AlterSheet` 基准 |
193193
| 旧式 `.xls`、`.xlsb` 与 `.ods` 格式 | 延后 | 否 |
194-
| Worksheet copy-and-add | 延后 | 否 |
194+
| 原子 workbook copy-and-add | 已支持 dynamic/schema/Serde path API | Rust source/destination rollback 测试;固定 .NET `CopyAndAddSheet` 基准 |
195195
| 高级 template、picture 与 merge | 延后 | 否 |
196196

197197
此矩阵就是覆盖声明:Rust 目前尚未提供与当前 .NET package 完整的 API 等价性。

‎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 `6df7623` (`0.3.0`) |
11+
| MiniExcel-Rust | working tree based on `db95441` (`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).
@@ -34,7 +34,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
3434
| Async and cancellation | Partial | Optional runtime-neutral dynamic/Serde path queries and explicit-schema Insert support bounded streams and cooperative cancellation. Async export/template operations, async write sources, borrowed async readers, 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. |
37-
| Existing-workbook operations | Partial | Rust atomically appends, strictly replaces, renames, changes visibility, or reorders worksheets while preserving unrelated package parts and worksheet identity. Copy-and-add remains unsupported. Rename preserves formula text; visibility rejects hiding the final visible sheet; reorder remaps active/view/local-name indices instead of transferring their ownership. |
37+
| 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. |
3838
| 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. |
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. |
@@ -55,7 +55,7 @@ Rust already implements dynamic and Serde-typed XLSX path queries, inclusive A1
5555
| Tables | `MiniExcel::query_table*`; Rust focused tests use the exact `TestQueryTable.xlsx` fixture (SHA-256 `04F719BF9F9E99D9B437A8FB32F8111FD92580A1D29ACAD10B6ED128C0564501`) | `OpenXmlImporter.QueryTableAsync`; `tests/MiniExcel.OpenXml.Tests/Tables/` |
5656
| Comments | `MiniExcel::get_comments*`; Rust focused tests use `TestCommentsAndNotes.xlsx` (SHA-256 `3A855CE896ED62DC27C91797432DD89EE081F07CD03AB05BF1B0CD745543A3FC`) | `OpenXmlImporter.RetrieveCommentsAsync`; `tests/MiniExcel.OpenXml.Tests/Comments/` |
5757
| 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/` |
58-
| Multi-sheet and workbook edits | Writer creates multiple sheets; existing workbooks support append, strict replacement, and all independent `AlterSheet` metadata operations with atomic package preservation | `OpenXmlExporter.InsertSheetAsync`, `CopyAndAddSheetAsync`, `AlterSheetAsync`; `tests/MiniExcel.OpenXml.Tests/MultipleSheets/` and `AlterSheets/` |
58+
| Multi-sheet and workbook edits | Writer creates multiple sheets; existing workbooks support append, strict replacement, every independent `AlterSheet` metadata operation, and source-to-destination `CopyAndAddSheet` with atomic package preservation | `OpenXmlExporter.InsertSheetAsync`, `CopyAndAddSheetAsync`, `AlterSheetAsync`; exporter, `MultipleSheets/`, and `AlterSheets/` tests |
5959
| Templates/pictures/merges | Basic template fill implemented; advanced directives and authoring remain deferred | `src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs`; `tests/MiniExcel.OpenXml.Tests/Templates/` |
6060
| CSV/conversion | `MiniExcel::query_csv*`, `save_csv*`, and `append_csv*`; Rust tests use exact `TestHeader.csv` (`6C2FC27FCA2876F1ECCA17061B8EE23E133ECDB726F8E0B84167E58D86234432`) and GB2312 (`BA8A2505AB271D5575C58CC1FCBE5A5002CEB9E2F43CB95412246E25A50E8B5A`) fixtures | `src/MiniExcel.Csv/Api/`; `src/MiniExcel/MiniExcelConverter.cs`; `tests/MiniExcel.Csv.Tests/` |
6161
| Mapping | Serde mapping only | `src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs`; `src/MiniExcel.OpenXml.FluentMapping/`; mapping tests |
@@ -68,7 +68,7 @@ The .NET APIs marked with `Async` also have generated synchronous counterparts t
6868

6969
1. **Async export/template APIs**: extend runtime-neutral producer/cancellation patterns without presenting blocking ZIP work as async I/O.
7070
2. **Advanced templates and Fluent Mapping**: add grouped/conditional templates, parametrized sheets, and mapping through separate compatibility milestones.
71-
3. **Remaining workbook edits**: copy-and-add requires a relationship-closure cloning contract.
71+
3. **Selected-sheet cloning**: not part of .NET `CopyAndAddSheet`; add only for a concrete Rust use case with a relationship-closure cloning contract.
7272

7373
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.
7474

@@ -78,6 +78,7 @@ DataReader/DataTable are .NET ecosystem abstractions and are intentionally not l
7878
- The .NET numeric `QueryRange` overload is not counted separately because Rust A1 `CellReference` bounds express the same selection.
7979
- Internal implementation classes and .NET-specific dependency-injection mechanics are excluded unless they expose observable behavior.
8080
- CSV DataReader/DataTable and a dedicated CSV/XLSX converter are excluded because Rust iterators and composed query/save calls provide the native equivalents.
81+
- Arbitrary selected-sheet cloning is not counted because .NET `CopyAndAddSheet` copies the whole source workbook and generates a sheet from row data; Rust now implements that observable operation.
8182
- Legacy binary Excel formats are excluded because this comparison found no current .NET V2 public provider that would make them a parity requirement.
8283

8384
Re-run this comparison whenever either pinned revision changes. New parity claims should be added to the shared contract only after both public adapters and focused tests cover the behavior.

0 commit comments

Comments
 (0)