Skip to content

Streaming writer handle, CLI tool, schema inference, and write/Arrow bindings for C++/Rust/Python - #93

Merged
GabrielMarquezMatte merged 52 commits into
masterfrom
develop
Aug 26, 2026
Merged

Streaming writer handle, CLI tool, schema inference, and write/Arrow bindings for C++/Rust/Python#93
GabrielMarquezMatte merged 52 commits into
masterfrom
develop

Conversation

@GabrielMarquezMatte

@GabrielMarquezMatte GabrielMarquezMatte commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Summary

Major feature batch since v2.1.3: a native streaming writer handle, a new excelreader
dotnet CLI tool, schema inference, and write/Arrow support extended to the C++, Rust, and
Python bindings (previously read-only there).

Core (C#)

  • Excel.InferSchema + ExcelColumnSchema/ExcelColumnType — samples a sheet and returns a
    usable ColumnSpec[], directly consumable by ParseTyped. Currently unshipped API.
  • Fixes: numeric/bool type loss on convert, a column-count-limit bypass, and FFI handle leaks.

Native ABI (C ABI, ExcelReader.Native)

  • Streaming writer handle: xl_open_write_handle[_to_memory], xl_start_sheet/xl_start_row,
    scalar xl_write_string/xl_write_int64/xl_write_float64/xl_write_bool/xl_write_date/
    xl_write_time/xl_write_timestamp/xl_write_null, xl_end_row/xl_end_sheet,
    xl_close_write_handle, xl_write_handle_bytes. Lets a caller build a workbook row-by-row
    instead of materializing a full columnar table first.
  • xl_write_typed_to_memory — in-memory typed write, mirroring the existing file-based path.
  • Removed two ABI exports that shipped unused by every binding (xl_next_row_decoded,
    xl_free_row) — XL_ABI_VERSION bumped 2 → 3; the equivalent per-row decode logic
    stays internal, backing xl_read_all_decoded.

CLI (new: ExcelReader.Cli, packaged as ExcelReader.NET.Cli)

  • excelreader sheets|schema|convert, with an interactive Spectre.Console table/spinner on a
    real terminal and identical plain tab-separated output when piped/redirected.
  • release.yml now actually packs and pushes ExcelReader.NET.Cli to NuGet — previously only
    self-contained AOT binaries were attached to GitHub Releases, so the README's documented
    dotnet tool install --global ExcelReader.NET.Cli would have failed.

C++ / Rust / Python bindings

  • C++: xl::write_columns, xl::write_sheet<T>, xl::WriterHandle (RAII over the new
    streaming ABI), xl::parse_arrow/ArrowTable, nullable<T> field support.
  • Rust: ExcelWriter + #[derive(ExcelMapper)]-generated writer, writer_handle module,
    parse_arrow behind the arrow feature, write benchmarks against rust_xlsxwriter.
  • Python: writer surface extended for the new typed-write options; streaming writer
    bindings intentionally not added
    — ctypes' per-call FFI cost doesn't fit a row-at-a-time
    API, and the existing columnar write_workbook/write_pandas/write_polars already cover
    the realistic Python write path.
  • New comparison benchmarks: C++ write vs. xlnt/xlsxio/libxlsxwriter/DuckDB.

Breaking changes

  • XL_ABI_VERSION 2 → 3. Any direct C ABI consumer that doesn't go through the shipped
    bindings should re-check xl_abi_version() before upgrading.
  • No changes to PublicAPI.Shipped.txt — the new managed API (InferSchema,
    ExcelColumnSchema, ExcelColumnType) is still tracked as Unshipped and will promote on
    the next tag.

Test plan

  • dotnet build ExcelReader.slnx (net8.0 + net10.0) — 0 warnings
  • dotnet test tests/ExcelReader.Tests — full suite passing
  • cargo test / cargo bench --no-run (rust/excelreader, rust/excelreader-derive)
  • pytest python/tests
  • dotnet pack src/ExcelReader.Cli verified locally — produces a valid dotnet-tool
    package (DotnetToolSettings.xml + Core/Cli/Spectre.Console DLLs under tools/)
  • cpp/tests — all passing

GabrielMarquezMatte and others added 30 commits August 20, 2026 12:34
@
docs: document the C++ and Rust writers

The root README still claimed the writers were not exposed across the ABI,
which stopped being true when the Python binding shipped write_workbook and
is now wrong for all three bindings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
Adds excelreader_cpp_write_compare_benchmarks under the existing
EXCELREADER_BUILD_BENCHMARKS_COMPARE flag: all four cases write the same
14-column, 65,535-row shape from the same in-memory rows, so the numbers
differ by writer rather than by workload. xlsxio's write side is built as
its own static library against the minizip compat shim the read side
already set up.

Also fixes benchmark_write.cpp's BM_WriteColumns, which wrote 4 columns
against BM_WriteSheet's 7. That made the columnar path look ~2x faster
when most of the gap was three fewer columns of work; with both at 7 the
real difference is a few percent.
The Python README had no benchmark section at all; adds read and write
tables from benchmarks/bench_read.py and bench_write.py, with the two
DataFrame comparisons (pandas to_excel, polars write_excel) labelled as
the only matched-work pairs and the Arrow-plus-pylist conversion the
DataFrame helpers pay called out explicitly.

The C++ standalone write table is the re-run after BM_WriteColumns was
corrected to write all 7 columns.
libxlsxwriter is a pure-C streaming writer (same author as
rust_xlsxwriter), the closest competitor class to ExcelReader's own
native core - worth measuring next to xlnt and xlsxio.

Its CMakeLists.txt is FetchContent-friendly except for one hard
dependency: find_package(ZLIB REQUIRED), with nothing to redirect it to.
madler/zlib has no Config-mode package and OVERRIDE_FIND_PACKAGE only
covers Config mode, so this fetches zlib itself and prepends a small
generated FindZLIB.cmake to CMAKE_MODULE_PATH that aliases the target
already built - a target alias rather than a hardcoded library path,
which is what makes it work under a multi-config generator (Visual
Studio) where the real .lib only exists once a config is chosen at
build time.

Verified end-to-end: configures, builds, links, and runs on Windows/MSVC.
One xlsxio run in this session logged an internal zip-creation error
without failing the benchmark, so that row in the README is marked
provisional pending a clean rerun - not something this change caused or
can fix.
Adds BM_DuckDB_Xlsx_Full to the read comparison and BM_DuckDB_Write to
the write comparison, both under the existing
EXCELREADER_BUILD_BENCHMARKS_COMPARE flag.

DuckDB is fetched as a prebuilt library (libduckdb-{windows,osx,linux}-*.
zip from its GitHub Releases) rather than built from source - its
amalgamated source is enormous and would dwarf every other FetchContent
build in this file, and DuckDB already publishes prebuilt binaries per
platform the same way this project's own excelreader-native-*.{dll,so,
dylib} assets are consumed (see cmake/FetchNativeLib.cmake).

Read side runs a single SQL aggregate over read_xlsx() - the idiomatic
way to make a SQL engine touch every cell, not a workaround. Write side
loads rows via DuckDB's Appender API before the timed region, then times
only COPY ... TO ... WITH (FORMAT xlsx), matching the "transpose outside
the loop" treatment BM_ExcelReader_WriteColumns already gets.

Every API call (Connection::Query, MaterializedQueryResult::GetValue,
Appender::AppendRow, date_t's day-count constructor) was checked against
DuckDB's actual headers and test suite before writing this, not
recalled from memory - not yet build-verified end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The symbol was already exported from the NativeAOT library via
[UnmanagedCallersOnly], but absent from the three hand-maintained .def
copies that lib.exe/dlltool turn into the Windows import library, so
C++ and Rust could not link against it. Adds a test asserting the three
copies stay identical.
GabrielMarquezMatte and others added 20 commits August 21, 2026 18:11
Wraps the existing xl_parse_arrow export. Kept in its own header so a
caller who does not want the Arrow C Data Interface declarations never
includes them, and deliberately free of any Apache Arrow C++ dependency:
the C Data Interface is the interop currency, so the caller feeds the
pair into whichever Arrow implementation they already link.
…arrow

Both native entry points take an identical xl_column_spec array. Pulls
the pointer bookkeeping (and the keepalive vectors the specs point into)
into one SpecArena so the upcoming Arrow binding does not copy it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Imports the native xl_parse_arrow export through the Arrow C Data
Interface and hands back an arrow::array::RecordBatch. Off by default,
matching the chrono feature: arrow-rs is a large dependency the typed
parse path needs none of.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- DefFileSyncTests: drop the cpp/include/xl symlink arm from the .def
  comparison (it resolves to the same file as the canonical copy, so it
  compared a file to itself and would break on Windows CI runners that
  don't enable core.symlinks). Only two .def copies are physically
  distinct; the remaining test still catches real drift.
- workbook.rs: make SpecArena generic and carry T::bindings() alongside
  the flat spec array, so build_specs::<T>() computes bindings exactly
  once instead of parse_sheet<T> calling T::bindings() a second time.
  arrow.rs is unaffected and compiles unmodified.
- Add C++ coverage for parse_arrow failing cleanly on an out-of-range
  header_row (cpp/tests/arrow.cpp), and strengthen the matching Rust
  test to prove the workbook is still usable after the failed call.
A thin shell over ExcelReader.Core's public API - nothing here parses a
spreadsheet byte. ConsoleAppFramework generates the parsing, routing and
--help (from XML doc comments, so help cannot drift from the signature);
it is a source generator referenced with PrivateAssets, so nothing ships
at runtime (verified via dotnet publish - only ExcelReader.Cli/.Core land
in the output). The command bodies live in CliCommands as plain functions
over explicit writers, keeping the tested surface clear of the
framework's static output hooks.

sheets lists a workbook's sheets; convert streams a sheet out as CSV (to
a file or stdout, via the existing CsvWorkbookWriter); schema prints the
inferred column schema via Excel.InferSchema. All three share Open()
(sheet selection by index or name) and Execute() (exit 0/1, stderr on
expected failures).

Deviations from the plan text, forced by the concrete framework version
(ConsoleAppFramework 5.7.13):
- delimiter is 'char? = null' resolved to ',' in Commands.Convert, not
  'char = \',\''  - the generator's own codegen mis-emits a char default
  literal that is itself a comma (var arg3 = (char),; - a real generator
  parsing bug), so the default lives in the adapter body instead.
- Commands' methods stay instance methods, not static: ConsoleAppFramework's
  app.Add<T>() rejects a class with only static commands (CAF012). CA1822
  and the CsvSheetWriter IDISP001 (End() is the real teardown, matching
  CsvWorkbookWriterTests.cs's own suppression) are scoped-NoWarn with
  comments in the csproj, not code changes.
- All three commands landed in one pass instead of three separate tasks -
  mechanical, same files, no reason to split the commits by command.

ExcelReader.Tests.csproj: the Cli ProjectReference and CliTests.cs are
net10.0-only (Condition on TargetFramework / Compile Remove) - the CLI
project is single-target and a dotnet tool has no net8.0 consumer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audited the CLI and native layer for bugs/vulnerabilities/simplifications
(agent-verified: parser memory safety, DoS, XXE all clean; three concrete
issues confirmed and fixed below).

CLI (src/ExcelReader.Cli/CliCommands.cs):
- convert wrote Number/Boolean cells through Write(string?) instead of the
  typed overloads, so every xlsx/xlsb/xls conversion target lost its
  numeric/boolean typing (Int64Column/Float64Column round-tripped back as
  StringColumn). Boolean cells store a raw '0'/'1' byte across every
  format (CellAccumulator.AddBool), not "True"/"False", so the fix reads
  that byte directly rather than through bool.TryParse.
- ResolveFormat: an --output with no extension reported "unrecognized
  extension '.'" (Path.GetExtension("noext") == ""), reading as if the
  user typed a bare dot. Now names the actual problem.
- convert now rejects an --output that names an existing directory with
  a clear message, instead of a directory-shaped path failing FileStream
  with a misleading "Access to the path is denied".

Core writer (XlsRowWriter.cs, XlsbRowWriter.cs):
- Write(int?)/Write(long?)/Write(double?)/Write(decimal?)'s null branch
  advanced _columnIndex directly instead of through Skip(1), bypassing
  the column-count bounds check every other nullable overload in the
  same class already goes through (Skip's own remarks describe fixing
  this exact bug - just missed on these four numeric overloads).

Native FFI (Exports.cs, NativeApi.Open/Typed/Arrow/Schema.cs):
- xl_close had no try/catch, unlike every other NativeApi entry point -
  an IOException from FileStream.Dispose (e.g. source volume gone) would
  unwind straight through the UnmanagedCallersOnly frame instead of
  returning XL_ERROR.
- xl_free_row/xl_free_rows/xl_free_table/xl_free_schema and both Arrow
  release callbacks are void in the ABI with no status code to report
  through; an exception escaping any of them is a fail-fast abort for
  the native caller, uncatchable in C/C++/Rust/Python. All six now catch
  and log to xl_last_error instead of propagating.
- FreeSchema dereferenced spec.Names without checking it for null first.
- BuildTable/BuildArrowSchema/BuildArrowArray leaked every already-built
  column/child when a later one threw (OOM is the realistic trigger -
  xl_parse_arrow briefly holds both the intermediate table and its Arrow
  copy at once). Each now releases what it already built before rethrowing.

Also strengthened three CliTests.cs assertions that would have passed on
wrong data (sheets listing checked only the first line; the delimiter
test checked "contains one ';'" rather than every field; the format
round-trip test checked only the first row's column count, not row
count or cell values).

WriterStateGuard.ValidateSheetName: internal -> private, one caller in
the same file.

Build clean, 1141/1141 tests (net10.0), 1112/1112 (net8.0).
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Benchmark Results

Measured on ubuntu-latest (GitHub Actions). Runner noise may affect absolute numbers; use these for relative comparisons within a PR.

ExcelReader.Benchmarks.ColdStartBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-GOEFXF : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

InvocationCount=1  IterationCount=1  LaunchCount=16  
RunStrategy=ColdStart  UnrollFactor=1  WarmupCount=0  

Method Rows Mean Error StdDev Allocated
TypedParseFirstUse 200 39.06 ms 1.998 ms 1.963 ms 28.38 KB
RecordWriteFirstUse 200 25.94 ms 0.197 ms 0.193 ms 84.27 KB
FluentParseFirstUse 200 35.36 ms 0.294 ms 0.289 ms 30.75 KB
FluentParseWithAttributeFallbackFirstUse 200 42.17 ms 0.329 ms 0.323 ms 32.45 KB

ExcelReader.Benchmarks.CsvParseBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
ExcelParserSync 50000 8.856 ms 0.4518 ms 0.1173 ms 1.00 0.02 156.2500 - 3.86 MB 1.00
ExcelParserAsync 50000 8.986 ms 0.2108 ms 0.0548 ms 1.01 0.01 156.2500 - 3.86 MB 1.00
Sep 50000 13.287 ms 0.2588 ms 0.0672 ms 1.50 0.02 156.2500 - 3.87 MB 1.00
Sylvan 50000 18.677 ms 0.5623 ms 0.1460 ms 2.11 0.03 437.5000 31.2500 10.95 MB 2.84
CsvHelperLib 50000 35.153 ms 0.8613 ms 0.1333 ms 3.97 0.05 600.0000 66.6667 14.41 MB 3.73

ExcelReader.Benchmarks.CsvReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
ExcelReader 50000 5.701 ms 0.0679 ms 0.0176 ms 1.00 0.00 - - 352 B 1.00
ExcelReaderWide 50000 14.123 ms 0.5047 ms 0.0781 ms 2.48 0.01 - - 288 B 0.82
ExcelReaderAsync 50000 5.540 ms 0.0444 ms 0.0115 ms 0.97 0.00 - - 424 B 1.20
Sep 50000 13.411 ms 0.1506 ms 0.0391 ms 2.35 0.01 - - 4024 B 11.43
Sylvan 50000 6.418 ms 0.0200 ms 0.0052 ms 1.13 0.00 93.7500 7.8125 1688701 B 4,797.45
CsvHelperLib 50000 37.391 ms 0.4491 ms 0.0695 ms 6.56 0.02 857.1429 71.4286 15073424 B 42,822.23

ExcelReader.Benchmarks.CsvWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.59GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
ExcelReaderWriter 50000 8.990 ms 0.3248 ms 0.0503 ms 1.00 500.0000 500.0000 500.0000 4 MB 1.00
Sep 50000 9.656 ms 0.1440 ms 0.0223 ms 1.07 500.0000 500.0000 500.0000 4.01 MB 1.00
SylvanWriter 50000 10.011 ms 0.1124 ms 0.0174 ms 1.11 500.0000 500.0000 500.0000 4.04 MB 1.01
CsvHelperLib 50000 23.362 ms 0.2545 ms 0.0394 ms 2.60 1000.0000 625.0000 593.7500 13.79 MB 3.44

ExcelReader.Benchmarks.ParseBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
ExcelParserSync 50000 24.05 ms 0.081 ms 0.021 ms 1.00 0.00 156.2500 3966.75 KB 1.000
ExcelParserSyncSharedStrings 50000 18.68 ms 0.060 ms 0.015 ms 0.78 0.00 93.7500 2357.23 KB 0.594
ExcelParserStructSync 50000 22.92 ms 0.198 ms 0.031 ms 0.95 0.00 62.5000 1623.02 KB 0.409
RefParserParseNamedSync 50000 19.54 ms 0.025 ms 0.004 ms 0.81 0.00 - 11.63 KB 0.003
ExcelParserAsync 50000 23.23 ms 0.106 ms 0.027 ms 0.97 0.00 156.2500 3968.9 KB 1.001
ExcelParserXlsbSync 50000 10.93 ms 0.108 ms 0.028 ms 0.45 0.00 156.2500 3968.84 KB 1.001
ExcelParserXlsbAsync 50000 10.99 ms 0.231 ms 0.060 ms 0.46 0.00 156.2500 3971.41 KB 1.001
MiniExcel 50000 217.76 ms 21.189 ms 3.279 ms 9.05 0.12 8000.0000 202525.77 KB 51.056
Sylvan 50000 88.66 ms 3.383 ms 0.524 ms 3.69 0.02 - 10726.71 KB 2.704
SylvanAsync 50000 89.59 ms 3.509 ms 0.543 ms 3.72 0.02 - 10727.74 KB 2.704

ExcelReader.Benchmarks.ReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
ExcelReader 50000 16.533 ms 0.1145 ms 0.0297 ms 1.00 0.00 - 11.13 KB 1.00
ExcelReaderAsync 50000 16.850 ms 0.0232 ms 0.0036 ms 1.02 0.00 - 13.26 KB 1.19
ExcelReaderXlsb 50000 6.406 ms 0.0376 ms 0.0098 ms 0.39 0.00 - 13.22 KB 1.19
ExcelReaderXlsbAsync 50000 6.769 ms 0.1190 ms 0.0309 ms 0.41 0.00 - 15.77 KB 1.42
ExcelReaderMaterialized 50000 19.671 ms 0.3171 ms 0.0823 ms 1.19 0.00 93.7500 1622.45 KB 145.84
MiniExcel 50000 225.332 ms 11.5986 ms 1.7949 ms 13.63 0.10 13000.0000 214012.34 KB 19,237.06
Sylvan 50000 57.036 ms 1.1211 ms 0.1735 ms 3.45 0.01 - 1939.18 KB 174.31

ExcelReader.Benchmarks.RealDataReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon 6973P-C 3.83GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Xlsx_ExcelReader 67.841 ms 4.6008 ms 1.1948 ms 1.00 0.02 - - 18222 B 1.00
Xlsx_Sylvan 197.263 ms 1.3162 ms 0.2037 ms 2.91 0.05 - - 659448 B 36.19
Xlsx_ExcelReader_Materialized 67.591 ms 0.1155 ms 0.0179 ms 1.00 0.02 - - 27918 B 1.53
Xlsx_ExcelReader_Prefetch 44.192 ms 5.1099 ms 0.7908 ms 0.65 0.01 - - 38432 B 2.11
Xlsx_ExcelReader_Memory 66.832 ms 0.0406 ms 0.0063 ms 0.99 0.02 - - 7110 B 0.39
Xlsx_ExcelReader_Memory_Prefetch 45.952 ms 9.5281 ms 2.4744 ms 0.68 0.04 - - 27208 B 1.49
Xlsm_ExcelReader 66.705 ms 2.4603 ms 0.3807 ms 0.98 0.02 - - 18222 B 1.00
Xlsm_Sylvan 195.668 ms 2.6878 ms 0.4159 ms 2.88 0.05 - - 659528 B 36.19
Xlsm_ExcelReader_Materialized 67.104 ms 0.4508 ms 0.0698 ms 0.99 0.02 - - 27918 B 1.53
Xlsm_ExcelReader_Prefetch 43.585 ms 6.3601 ms 0.9842 ms 0.64 0.02 - - 38320 B 2.10
Xlsm_ExcelReader_Memory 66.429 ms 1.7640 ms 0.2730 ms 0.98 0.02 - - 7110 B 0.39
Xlsm_ExcelReader_Memory_Prefetch 42.475 ms 13.0252 ms 2.0157 ms 0.63 0.03 - - 27208 B 1.49
Xlsb_ExcelReader 25.822 ms 0.5009 ms 0.1301 ms 0.38 0.01 - - 19192 B 1.05
Xlsb_Sylvan 30.356 ms 0.0959 ms 0.0249 ms 0.45 0.01 - - 346662 B 19.02
Xlsb_ExcelReader_Materialized 26.875 ms 0.0894 ms 0.0138 ms 0.40 0.01 - - 28888 B 1.59
Xlsb_ExcelReader_Prefetch 18.589 ms 0.5030 ms 0.1306 ms 0.27 0.00 - - 62876 B 3.45
Xlsb_ExcelReader_Memory 25.488 ms 0.1003 ms 0.0260 ms 0.38 0.01 - - 8840 B 0.49
Xlsb_ExcelReader_Memory_Prefetch 18.474 ms 0.4438 ms 0.0687 ms 0.27 0.00 - - 48548 B 2.66
Xls_ExcelReader 13.350 ms 0.1361 ms 0.0353 ms 0.20 0.00 - - 12048 B 0.66
Xls_Sylvan 16.416 ms 0.1320 ms 0.0343 ms 0.24 0.00 - - 190366 B 10.45
Xls_ExcelReader_Materialized 14.117 ms 0.0852 ms 0.0132 ms 0.21 0.00 - - 21744 B 1.19
Xls_ExcelReader_Memory 9.237 ms 0.0818 ms 0.0213 ms 0.14 0.00 - - 12048 B 0.66
Csv_ExcelReader 5.168 ms 0.0157 ms 0.0041 ms 0.08 0.00 - - 288 B 0.02
Csv_Sylvan 13.989 ms 2.5096 ms 0.3884 ms 0.21 0.01 437.5000 46.8750 37483926 B 2,057.07
Csv_ExcelReader_Materialized 22.965 ms 0.4242 ms 0.1102 ms 0.34 0.01 437.5000 - 37442896 B 2,054.82
Csv_ExcelReader_Memory 4.883 ms 0.2196 ms 0.0570 ms 0.07 0.00 - - 224 B 0.01

ExcelReader.Benchmarks.RecordWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.59GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Xlsx 50000 25.626 ms 0.3672 ms 0.0954 ms 1.00 500.0000 500.0000 500.0000 4.02 MB 1.00
Xlsb 50000 10.220 ms 0.1831 ms 0.0476 ms 0.40 500.0000 500.0000 500.0000 4.02 MB 1.00
Xls 50000 7.095 ms 0.3022 ms 0.0785 ms 0.28 273.4375 273.4375 273.4375 4.03 MB 1.00
Csv 50000 10.267 ms 0.1970 ms 0.0512 ms 0.40 500.0000 500.0000 500.0000 4 MB 1.00

ExcelReader.Benchmarks.WriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.59GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
ExcelReaderWriter 50000 22.354 ms 0.3014 ms 0.0466 ms 1.00 0.00 500.0000 500.0000 500.0000 4.02 MB 1.00
ExcelReaderWriterSharedStrings 50000 22.535 ms 0.0904 ms 0.0235 ms 1.01 0.00 500.0000 500.0000 500.0000 4.06 MB 1.01
ExcelReaderWriterPrefetch 50000 19.054 ms 3.1176 ms 0.8096 ms 0.85 0.03 468.7500 468.7500 468.7500 4.03 MB 1.00
ExcelReaderXlsbWriter 50000 9.728 ms 0.1243 ms 0.0192 ms 0.44 0.00 500.0000 500.0000 500.0000 4.02 MB 1.00
ExcelReaderXlsbWriterSharedStrings 50000 9.463 ms 0.6113 ms 0.1587 ms 0.42 0.01 500.0000 500.0000 500.0000 4.06 MB 1.01
ExcelReaderXlsbWriterPrefetch 50000 8.850 ms 1.3946 ms 0.3622 ms 0.40 0.01 484.3750 484.3750 484.3750 4.03 MB 1.00
MiniExcel 50000 101.534 ms 2.2437 ms 0.3472 ms 4.54 0.02 3200.0000 600.0000 600.0000 84.89 MB 21.11
SpreadCheetah 50000 23.586 ms 0.1644 ms 0.0254 ms 1.06 0.00 968.7500 500.0000 500.0000 15.84 MB 3.94

ExcelReader.Benchmarks.XlsReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
ExcelReader 50000 5.461 ms 0.0062 ms 0.0010 ms 1.00 - - 2.91 KB 1.00
ExcelReaderAsync 50000 5.473 ms 0.0192 ms 0.0030 ms 1.00 - - 2.98 KB 1.02
Sylvan 50000 8.484 ms 0.0286 ms 0.0044 ms 1.55 93.7500 15.6250 1717.73 KB 589.46

ExcelReader.Benchmarks.XlsWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz (Max: 2.59GHz), 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4
  Job-MEHJPP : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
XlsWriter 50000 7.355 ms 0.2534 ms 0.0658 ms 1.00 0.01 492.1875 492.1875 492.1875 16.03 MB 1.00
XlsxWriter 50000 22.752 ms 0.0233 ms 0.0060 ms 3.09 0.03 500.0000 500.0000 500.0000 4.02 MB 0.25

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 64.73988% with 244 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.17%. Comparing base (9a244f1) to head (47c0d6f).

Files with missing lines Patch % Lines
src/ExcelReader.Native/Exports.cs 0.84% 117 Missing ⚠️
src/ExcelReader.Cli/Commands.cs 0.00% 33 Missing ⚠️
...rc/ExcelReader.Native/Writer/NativeWriterHandle.cs 76.31% 14 Missing and 4 partials ⚠️
src/ExcelReader.Native/NativeApi.Arrow.cs 65.95% 15 Missing and 1 partial ⚠️
src/ExcelReader.Cli/CliCommands.cs 91.56% 7 Missing and 7 partials ⚠️
src/ExcelReader.Native/NativeApi.Write.cs 82.60% 12 Missing ⚠️
src/ExcelReader.Cli/ColorizingErrorWriter.cs 0.00% 10 Missing ⚠️
src/ExcelReader.Native/NativeApi.Typed.cs 50.00% 5 Missing and 1 partial ⚠️
src/ExcelReader.Cli/ErrorConsole.cs 0.00% 4 Missing ⚠️
src/ExcelReader.Core/Writer/XlsRowWriter.cs 66.66% 4 Missing ⚠️
... and 5 more
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #93      +/-   ##
==========================================
- Coverage   86.56%   85.17%   -1.40%     
==========================================
  Files         129      136       +7     
  Lines        9533    10041     +508     
  Branches     1789     1868      +79     
==========================================
+ Hits         8252     8552     +300     
- Misses        958     1159     +201     
- Partials      323      330       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@GabrielMarquezMatte
GabrielMarquezMatte merged commit b7eb616 into master Aug 26, 2026
39 checks passed
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