Skip to content

fix(table): byte-preserving row/column deletion - #208

Merged
obvious-autobuild-staging[bot] merged 1 commit into
masterfrom
fix/table-deletion-bytes
Sep 8, 2026
Merged

fix(table): byte-preserving row/column deletion#208
obvious-autobuild-staging[bot] merged 1 commit into
masterfrom
fix/table-deletion-bytes

Conversation

@obvious-autobuild-staging

Copy link
Copy Markdown
Contributor

Why

PR #207 shipped row/column deletion, but both transforms in app/src/lib/tableDeletion.ts filter the parsed grid and re-serialize the entire table through serializeCsvTable. On any row or column deletion that silently:

  • normalizes CRLF line endings to LF,
  • strips the UTF-8 BOM,
  • strips the trailing newline,
  • drops ragged-row extra fields (x,y,z,EXTRA1,EXTRA2x,y,z), and
  • erases the malformed-CSV warning for surviving ragged rows (the file comment even normalized rowWidths to "never trip the malformed-CSV warning detector" — that normalization is what made the warning disappear).

That is data loss, and it contradicts the byte-preservation contract the cell-edit path already honors in csvTable.ts ("untouched rows keep their exact bytes"). Verified against master (9a1579a3) before this branch.

What

Deletion now splices the raw source text using the same primitives the cell-edit commit path already proved out — BOM-aware rowSpans, parseCsvRecord, serializeCsvRow. No new parsing, no grid re-serialization.

Row deletiondeleteRows(raw, table, sources): each removed region is the row's span plus its own trailing terminator. One generic rule covers every terminator edge:

Case Behavior
First data row removes its own trailing terminator (the preceding one belongs to the header)
Middle row takes its trailing terminator, so the next row's leading bytes stay exact
Last row, file ends with newline the trailing terminator goes with it — no stray blank line
Last row, no trailing newline just the span; the surviving last row's own terminator becomes the ending
Delete all rows headers-only file: exactly the header's bytes plus its terminator

Mixed endings keep each row's own terminator, and the BOM is untouched because splices never touch index 0.

Column deletiondeleteColumns(raw, table, dataCols): per-record splice. parseCsvRecord over each raw row span, drop the selected fields, serializeCsvRow the record back. Records that lack every dropped field (short ragged rows) stay byte-identical — no splice at all; only records actually containing the field get their own bytes rewritten. The header gets the same treatment as one record (its span is derived from the parse's BOM-aware span base plus the single terminator before row 0). Wide ragged rows keep their extra fields, since extras sit past the header-defined columns and only those are deletable.

API change: the old functions took the parsed CsvTableData and returned a string; byte splicing needs the raw text plus the parse, so the signatures are now deleteRows(raw, parse, sources) / deleteColumns(raw, parse, dataCols). The three CsvTable.tsx call sites (toolbar rows, toolbar columns, keyboard onDelete) now pass latestRef.current.text alongside the table. The guarded-reconversion path, selection-state dropping, last-column guard, and no-undo-stack posture are unchanged; the output table stays read-only.

Deliberately NOT done: no new changelog entry — entry 10 (live <1 day) already describes the correct behavior users now actually get; this PR closes the gap between the description and the implementation rather than announcing it twice.

How to Review

  • app/src/lib/tableDeletion.ts — the whole fix. deleteRows maps source indices → spans → splice regions (deduped, sorted); deleteColumns walks header span + row spans and rewrites only records containing a dropped field. headerSpanOf derives the header record's span arithmetically (BOM base; one terminator back from row 0's span start, or trailing-terminator trim when there are no data rows).
  • app/src/lib/__tests__/tableDeletion.test.ts — the old normalized-output tests are replaced with the byte-fidelity contract: CRLF/BOM/no-trailing-newline/last-row/delete-all-rows, mixed endings, ragged rows on both deletion types, quoted fields with embedded delimiters/newlines sharing a file with plain rows, out-of-range tolerance, and csvWarnings persistence (surviving ragged rows keep their warning; deleting the malformed row clears it naturally).
  • app/src/components/CsvTable.tsx — callers only, plus updated comments describing the splice path.

Non-obvious choice: after deleting the last row of a file without a trailing newline, the file gains a trailing newline (the surviving row's own terminator). That is the byte-faithful outcome — every surviving byte stays exactly where it was; the alternative (also deleting the header's terminator to preserve file-level "absence") would alter bytes the deletion never touched.

Test Evidence

Local gates, all green on the tested head 4508028de1d21e7ba542825cc78045c7bde7099e: full Vitest suite 453/453 (was 439 + 23 new − 9 replaced), npm run lint, npx tsc -b --noEmit, npm run build with app/dist rebuilt and committed, bash .github/scripts/verify-seo.sh.

Dogfood (recorded against the tested head, fixtures ingested through the live app's file-upload control — the byte-honest ingest path, since textarea/fill APIs normalize CRLF before the app ever sees them):

  • tc-1 — CRLF column deletion, raw stays CRLF (pass): CRLF fixture uploaded, YEAR column selected and deleted via the toolbar, raw view shows the remaining columns. Byte-verified through the app's own download payload: name,album\r\nJack White,Blunderbuss\r\n…\r\ncr: true on every line.
  • tc-2 — ragged row deletion, extra fields survive (pass): ragged fixture (r,s,t,EXTRA1,EXTRA2), row 1,2,3 selected via the row marker and deleted. Byte-verified: raw state is x,y,z\nr,s,t,EXTRA1,EXTRA2\n4,5,6 — extras intact, so the malformed-CSV warning for that row persists.

🔗 Obvious Project · 🧵 Obvious Thread

Deletion spliced the parsed grid and re-serialized the whole table,
normalizing CRLF to LF, stripping the UTF-8 BOM, stripping the trailing
newline, dropping ragged-row extra fields, and erasing malformed-CSV
warnings. Replace it with raw-text splicing on the existing csvTable.ts
primitives (BOM-aware rowSpans, parseCsvRecord, serializeCsvRow) — the
same byte-fidelity contract the cell-edit commit already honors.

Row deletion removes each span plus its own line terminator; column
deletion rewrites only records that contain a dropped field. Untouched
rows keep their exact bytes, so surviving ragged rows keep their
malformed-CSV warnings.
@obvious-autobuild-staging
obvious-autobuild-staging Bot marked this pull request as ready for review September 8, 2026 13:51
@obvious-autobuild-staging
obvious-autobuild-staging Bot merged commit 7857f82 into master Sep 8, 2026
2 checks passed
@obvious-autobuild-staging
obvious-autobuild-staging Bot deleted the fix/table-deletion-bytes branch September 8, 2026 13:55
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.

1 participant