Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
a5bbfb0
fix: export xl_write_typed from the Windows .def
Aug 20, 2026
8e175ff
feat(cpp): add xl::write_columns, a zero-copy columnar write
Aug 20, 2026
5789139
fix(cpp): decode bool fields from the XL_T_BOOL byte buffer, not as i…
Aug 20, 2026
387cfa5
feat(cpp): support std::optional fields for nullable columns
Aug 20, 2026
aaf0293
feat(cpp): add xl::write_sheet<T> over the existing ExcelMapper
Aug 20, 2026
9f13866
feat(rust): declare xl_write_typed and add WriteOptions
Aug 20, 2026
3868b97
feat(rust): add ExcelWriter and writer::write_sheet
Aug 20, 2026
68840ff
feat(rust): generate ExcelWriter from #[derive(ExcelMapper)]
Aug 20, 2026
bd0e96d
bench(rust): add write benchmarks against rust_xlsxwriter
Aug 20, 2026
6a24fa6
bench(cpp): add write benchmarks for write_columns and write_sheet
Aug 20, 2026
fd3af87
@
Aug 20, 2026
481fecd
bench(cpp): compare writing against xlnt and xlsxio
Aug 20, 2026
6c3f128
docs: publish the Python and C++ write benchmark numbers
Aug 20, 2026
5f8f4d5
bench(cpp): add libxlsxwriter to the write comparison
Aug 20, 2026
0988ea8
bench(cpp): compare against DuckDB via its excel extension
Aug 20, 2026
3c92cad
Fixing xlsxio benchmark
Aug 20, 2026
e5fa125
Updating README with duckdb data
Aug 20, 2026
e645d4b
chore: gitignore .superpowers/ (subagent-driven-development scratch)
Aug 20, 2026
d5f1228
refactor(native): share the byte/tri-state option decoders between bo…
Aug 21, 2026
2d7ebb7
refactor(native): share the skip-to-header-row loop between parse_typ…
Aug 21, 2026
4c784ba
refactor(cpp): collapse the six scalar column factories onto one cons…
Aug 21, 2026
039a5d4
refactor(cpp): share the options-lowering block across open/open_memo…
Aug 21, 2026
bdbfdb1
refactor(cpp): route TableView's row materialization through one deta…
Aug 21, 2026
dcd0a67
Adding env variable for rust
Aug 21, 2026
30713df
refactor(python): inherit the inferred-spec layout and derive the for…
Aug 21, 2026
bdf1e1a
refactor(rust): share the raw-options pointer helper across open/open…
Aug 21, 2026
1d42180
refactor(rust): collapse the duplicated rejection arms in the ExcelMa…
Aug 21, 2026
8d8ace8
Parsing fixes
Aug 21, 2026
b783d28
Fixing dependencies
GabrielMarquezMatte Aug 21, 2026
58e85dc
fix: export xl_parse_arrow from the Windows .def files
GabrielMarquezMatte Aug 21, 2026
dc9afa4
feat(cpp): add xl::parse_arrow and the ArrowTable RAII owner
GabrielMarquezMatte Aug 21, 2026
83a30ef
refactor(rust): extract build_specs, shared by parse_typed and parse_…
GabrielMarquezMatte Aug 22, 2026
d964b0f
feat(rust): add parse_arrow behind the arrow feature
GabrielMarquezMatte Aug 22, 2026
b3ca97e
docs: the Arrow export is no longer Python-only
GabrielMarquezMatte Aug 22, 2026
dea92ca
docs(fix): use Row struct name instead of undefined Record in Arrow e…
GabrielMarquezMatte Aug 22, 2026
7fc494b
fix: final review fixes for Arrow C++/Rust bindings branch
GabrielMarquezMatte Aug 22, 2026
7fc8f80
Optimizing string allocation
GabrielMarquezMatte Aug 22, 2026
c155935
Implementing schema inference for core C#
Aug 24, 2026
6106394
feat(cli): add the excelreader dotnet tool (sheets, convert, schema)
Aug 24, 2026
cbe5176
docs: document the excelreader CLI
Aug 24, 2026
0fe7548
Using block methods
Aug 24, 2026
aa6b2ff
Adding new types for optimized writing
Aug 24, 2026
0d31b0f
Adding nice formatting for console output
Aug 24, 2026
721f454
fix: numeric/bool type loss on convert, column-limit bypass, FFI leaks
Aug 24, 2026
0e33588
Creating release for cli
Aug 25, 2026
263ef64
Implementing native streaming writing
GabrielMarquezMatte Aug 25, 2026
c2adf1f
In memory writing and C++ bindings
GabrielMarquezMatte Aug 25, 2026
a8e8777
Creating bindings for Rust in memory and streaming writers
GabrielMarquezMatte Aug 25, 2026
7e705e6
Removing unnecessary functions
GabrielMarquezMatte Aug 25, 2026
cb0d650
Merge branch 'master' into develop
GabrielMarquezMatte Aug 25, 2026
90947c2
Bump XL_ABI_VERSION
Aug 26, 2026
47c0d6f
Fixing formatting
Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ jobs:
-p:Version=${{ steps.version.outputs.value }}
--output ./artifacts

# Portable IL tool package (see the csproj: "a dotnet tool ships as IL and is launched by
# whatever SDK the user has") - not the self-contained AOT binaries publish-cli attaches to
# the GitHub Release below, which need no .NET runtime installed but must be built per-RID.
# `dotnet tool install --global ExcelReader.NET.Cli` (README's documented install command)
# resolves this package.
- name: Pack CLI
run: >-
dotnet pack src/ExcelReader.Cli/ExcelReader.Cli.csproj
--configuration Release
--no-restore
-p:Version=${{ steps.version.outputs.value }}
--output ./artifacts

- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
Expand Down Expand Up @@ -127,6 +140,76 @@ jobs:
files: ${{ steps.asset.outputs.name }}
fail_on_unmatched_files: true

publish-cli:
name: Publish excelreader CLI (${{ matrix.os }})
needs: publish
runs-on: ${{ matrix.os }}
permissions:
contents: write
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

- name: Derive version from tag
id: version
shell: bash
run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Determine RID
id: rid
shell: bash
run: |
arch=$([ "$(uname -m 2>/dev/null || echo x64)" = "arm64" ] && echo arm64 || echo x64)
case "${{ runner.os }}" in
Windows) os=win ;;
macOS) os=osx ;;
*) os=linux ;;
esac
echo "value=${os}-${arch}" >> "$GITHUB_OUTPUT"

# PublishAot/PackAsTool=false override the project's normal dotnet-tool packaging (a
# dotnet tool ships as IL, launched by the caller's own SDK) with a single self-contained
# native executable instead - no .NET runtime required on the machine that runs it.
# ConsoleAppFramework is already compile-time only and Spectre.Console is AOT-compatible,
# so this publishes clean with zero IL2xxx/IL3xxx trim warnings.
- name: Publish native AOT binary
shell: bash
run: >-
dotnet publish src/ExcelReader.Cli/ExcelReader.Cli.csproj
--configuration Release
-r ${{ steps.rid.outputs.value }}
-p:PublishAot=true
-p:PackAsTool=false
-p:Version=${{ steps.version.outputs.value }}
--output ./cli-publish

- name: Rename asset for this platform
id: asset
shell: bash
run: |
case "${{ runner.os }}" in
Windows) src=ExcelReader.Cli.exe; ext=.exe ;;
*) src=ExcelReader.Cli; ext= ;;
esac
name="excelreader-${{ steps.rid.outputs.value }}${ext}"
cp "./cli-publish/${src}" "${name}"
echo "name=${name}" >> "$GITHUB_OUTPUT"

- name: Upload to the GitHub Release
uses: softprops/action-gh-release@v3
with:
files: ${{ steps.asset.outputs.name }}
fail_on_unmatched_files: true

publish-rust:
name: Publish Rust crates
needs: publish-native-assets
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,6 @@ python/src/excelreader/_lib/*

# CMake build output (tests/ExcelReader.NativeSmoke and any other CMake-configured project). Never
# built to a path inside the source tree other than this, so a plain top-level rule is enough.
build/
build/
# Subagent-driven-development scratch (per-plan ledgers, briefs, review packages)
.superpowers/
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@
"-DEXCELREADER_BUILD_BENCHMARKS_COMPARE=ON",
"-DEXCELREADER_BUILD_TESTS=ON",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
]
],
"rust-analyzer.server.extraEnv": {
"EXCELREADER_NATIVE_LIB_DIR": "${workspaceFolder}/python/src/excelreader/_lib"
}
}
22 changes: 22 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ Reader internals that would otherwise be duplicated four times over live in one
the single buffer-growth-cap function (`NextBufferSize`) every pooled buffer in the stack grows
through, so one limit policy governs all of them consistently.

## The `excelreader` CLI

`src/ExcelReader.Cli/` is a thin `dotnet tool` shell (`excelreader`) over Core's public API — it
parses no spreadsheet bytes of its own. It splits in two on purpose: `Commands` is a one-line-per-
command adapter whose XML doc comments ConsoleAppFramework's source generator turns into argument
parsing, routing and `--help`, while `CliCommands` holds the bodies as plain functions over explicit
writers. The split keeps the tested surface free of the framework's static output hooks, so the CLI
tests run in parallel like every other test class. ConsoleAppFramework is compile-time only
(`PrivateAssets`), so the published tool's only *runtime* dependency besides ExcelReader.Core is
Spectre.Console, used for `sheets`/`schema`'s tables and `convert`'s progress spinner on a real
terminal.

Rendering follows `Commands`/`CliCommands`'s own split, one level further: `Commands` picks plain vs.
interactive per call (`Console.IsOutputRedirected`/`IsErrorRedirected`), so a script gets the exact
same tab-separated text and stderr line the tool always wrote, unchanged. Two small always-stderr
helpers back that split - `ErrorConsole` (a Spectre `IAnsiConsole` pinned to `Console.Error`, since
Spectre's own default instance targets stdout, which `convert` may be using for the converted bytes
themselves) and `ColorizingErrorWriter` (a `TextWriter` that renders `CliCommands.Execute`'s one-line
failure in red through `ErrorConsole` on a terminal, or passes it through byte-for-byte otherwise).
Both live in `ExcelReader.Cli`, not `CliCommands.cs` - the interactive/plain decision is
`Console`-shaped state, exactly what that file's tests are built to never touch.

## Why readers are split into partial classes

`XlsxReader` and `XlsbReader` are large enough that one file would be unwieldy, so each is split by
Expand Down
1 change: 1 addition & 0 deletions ExcelReader.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/ExcelReader.Cli/ExcelReader.Cli.csproj" />
<Project Path="src/ExcelReader.Core/ExcelReader.Core.csproj" />
<Project Path="src/ExcelReader.Native/ExcelReader.Native.csproj" />
<Project Path="src/ExcelReader.Generator/ExcelReader.Generator.csproj" />
Expand Down
57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ ExcelReader is built for streaming spreadsheet workloads where low allocations m
dotnet add package ExcelReader.NET
```

## Command line

```bash
dotnet tool install --global ExcelReader.NET.Cli
```

```bash
excelreader sheets book.xlsb # 0<TAB>Sheet1
excelreader schema book.xlsb --sample-size 500 # 0<TAB>Id<TAB>Int64
excelreader convert book.xlsb --output data.csv # convert to another format, by extension
excelreader convert book.xlsb --output data.xlsx # .xlsx, .xlsb, .xls and .csv all work
excelreader convert book.xlsb --format xlsx | head -c 4 # to stdout, --format picks it instead
```

Flags: `--sheet|-s <name|index>`, `--header-row N` (0 = no header), `--sample-size N`,
`--output|-o <file>`, `--format|-f <xlsx|xlsb|xls|csv>` (defaults to `--output`'s extension, or csv
for stdout), `--delimiter|-d <char>` (csv only). Run `excelreader <command> --help` for the full list.

On a real terminal, `sheets`/`schema` render as a [Spectre.Console](https://spectreconsole.net) table
and `convert` shows a progress spinner on stderr; piped or redirected (a script, `| head`, `2>file`),
every command falls back to the same plain tab-separated text and stderr line it always wrote, so
nothing here changes for anything already parsing this tool's output.

Exit codes are `0` ok and `1` failure; results go to stdout and errors to stderr, so `convert` is
safe to pipe.

## Read rows

```csharp
Expand Down Expand Up @@ -863,13 +889,13 @@ dotnet test --project tests/ExcelReader.Tests/ExcelReader.Tests.csproj --configu

## Other languages

ExcelReader ships a NativeAOT shared library with a C ABI, so non-.NET languages can read XLSX,
XLSB, XLS and CSV without a .NET runtime installed.
ExcelReader ships a NativeAOT shared library with a C ABI, so non-.NET languages can read and write
XLSX, XLSB, XLS and CSV without a .NET runtime installed.

- C ABI header: [`src/ExcelReader.Native/include/excelreader.h`](src/ExcelReader.Native/include/excelreader.h)
- Python package: [`python/`](python/README.md)
- C++ package: header-only CMake wrapper, `xl::Workbook`/`xl::parse_sheet` over the same ABI — see [`cpp/README.md`](cpp/README.md).
- Rust crate: safe `Workbook`/`parse_sheet` bindings, downloadable via `cargo add excelreader` — see [`rust/excelreader/README.md`](rust/excelreader/README.md).
- C++ package: header-only CMake wrapper, `xl::Workbook`/`xl::parse_sheet`/`xl::write_sheet` over the same ABI — see [`cpp/README.md`](cpp/README.md).
- Rust crate: safe `Workbook`/`parse_sheet`/`write_sheet` bindings, downloadable via `cargo add excelreader` — see [`rust/excelreader/README.md`](rust/excelreader/README.md).

```python
from excelreader import open_workbook
Expand All @@ -879,7 +905,28 @@ with open_workbook("book.xlsx") as workbook:
print([cell.value for cell in row])
```

Reading only — the writers are not exposed across the ABI yet.
Writing goes through one export, `xl_write_typed`: a whole sheet in a single call, from columnar
buffers the ABI borrows rather than copies. All three bindings expose it — Python as
`write_workbook`/`write_arrow`/`write_pandas`/`write_polars`, C++ as `xl::write_columns` and
`xl::write_sheet<T>`, Rust as `writer::write_columns` and `writer::write_sheet`. In C++ and Rust the
same struct mapping drives both directions, so reading a sheet and writing it back needs one
mapping, not two:

```rust
use excelreader::writer::write_sheet;
use excelreader::XL_FORMAT_XLSX;

write_sheet("out.xlsx", XL_FORMAT_XLSX, &rows, None)?;
```

```cpp
auto written = xl::write_sheet("out.xlsx", rows); // format inferred from the extension
```

Row-by-row decoded reads remain Python-only. The Arrow export is available from Python
(`to_arrow`/`to_record_batch`), C++ (`xl::parse_arrow<T>`, in the separate `<xl/excelreader_arrow.hpp>`
header — no Apache Arrow C++ dependency, you get the raw C Data Interface pair), and Rust
(`excelreader::arrow::parse_arrow`, behind the `arrow` cargo feature, returning an `arrow::array::RecordBatch`).

## Contributing

Expand Down
Loading
Loading