Skip to content

feat(write): write csv through TabularWriter, round-tripping through the import - #77

Merged
vaceslav merged 17 commits into
mainfrom
feat/write-csv
Oct 3, 2026
Merged

vaceslav merged 17 commits into
mainfrom
feat/write-csv

Conversation

@vaceslav

@vaceslav vaceslav commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

What and why

Closes #70. Part 1 of 5 of #67: writing tabular files, starting with csv. A caller writes a table row by row through TabularWriter into any stream — a file, a blob, an HTTP response body — and importing the file through TabularImporter gives back the same values with the same types.

Spec: docs/superpowers/specs/2026-10-03-writing-design.md. Plan: docs/superpowers/plans/2026-10-03-writing-part-1-csv.md.

  • TabularWriter (format-agnostic): BeginSheet / BeginRow / typed Write overloads (string?, long, decimal, double, DateTime, DateOnly, bool) / WriteEmpty / EndRow, then CompleteAsync. Writes go synchronously into a pooled in-memory buffer; only FlushAsync, CompleteAsync and DisposeAsync touch the target, always asynchronously, so it serves an ASP.NET Core response body. Flush whenever FlushRecommended is true and memory stays flat in the row count. A file is valid only after CompleteAsync; any exception faults the writer.
  • csv: invariant by default (,, ISO dates, UTF-8 with BOM), or a culture (de-DE → ;, 1234,5, 03.10.2026). A culture is accepted only if a probe of its numbers and dates, written as the writer writes them, reads back through the import's own ValueReading. Every delimiter the reader's dialect detector could pick is quoted, so a value can never sway detection. Opt-in FormulaGuard against csv injection.
  • Values the import would not give back are refused with a TabularWriteException carrying a code and the sheet, row and column: write.precision-loss (a double past 15 significant digits), write.not-finite, write.date-out-of-range (year 1), write.text-too-long, write.too-many-lines, write.ambiguous-line-breaks (multi-line text the reader's stray-quote recovery would split into records), write.invalid-character (characters XML 1.0 forbids — one rule for every format). The documented exceptions — trimmed text, milliseconds, DateTime.Kind, DateOnly → DateTime — are in KNOWN-ISSUES.

Performance. Not yet benchmarked formally (part 5, #74). A spot check in Release, 6 columns (long, quoted text, decimal, date-time, bool, double) to Stream.Null: about 260 ns per row, about 0.5 s per million rows; about 97 KB allocated whether 100k or 1M rows are written — nothing per row (values are formatted with TryFormat into a reused buffer). The read path is unchanged.

Tests: round trips through the import under the invariant, de-DE and en-US cultures (every value kind and its edge values); a seeded round-trip fuzz (3 cultures × 6 layouts × 300 rows) asserting that whatever the writer accepts the import reads back; a target stream that refuses every synchronous operation; stream ownership, faulting, cancellation, misuse. The fuzz fails when either csv fix from the final review is reverted.

Known follow-up, reader side: when the 64 KB dialect probe ends inside a multi-line quoted field, the detector stops honouring quotes.

Checklist

  • A test that failed before the change and passes after it (for a fix or a new behaviour).
  • dotnet build and dotnet test pass on net8.0 and net10.0 with zero warnings. (2126 tests)
  • If the read path changed: measured, and the numbers are in the description. (read path unchanged)
  • If an error code was added or changed: ErrorCodes and the guide's error-code table agree.
  • Public API changes are described, and breaking ones are called out. (additive only: TabularWriter, TabularWriterOptions, WriteColumn, CsvWriterOptions, TabularWriteException, ErrorCodes.Write)
  • No third-party package in src/.

…t it would split into records, and leave headers unguarded
…complete after completing, close the target if disposal fails
@vaceslav
vaceslav merged commit 5de3450 into main Oct 3, 2026
9 checks passed
@vaceslav
vaceslav deleted the feat/write-csv branch October 3, 2026 19:22
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.

Writing part 1: TabularWriter and csv

1 participant