Pre-1.0: a minor version may rename or remove. When it does, the migration is one line here.
0.1.3 (2026-09-18)
A second external review pass over 0.1.2, all correctness or safety, all in the
.xlsx codec unless said otherwise.
- A built-in elapsed format (46,
[h]:mm:ss) still lost whole days. 0.1.2 fixed the custom-format route but the built-in table kept a hardcoded:time; both routes now read the format code through one classifier, so 36 hours stays the number1.5rather than a 12-hourTime. - An ISO time-only cell (
t="d"holding12:30:00, fromiso_dates) read as nil and was erased on the next write; it now reads as aTime. - A standalone error cell is preserved on write. 0.1.2 stopped the crash but
blanked the cell on an unrelated edit; the
t="e"cell is now written back as it was read. Sheetshow.Store.Localcreated its temporary file, wrote the bytes, then made it private, leaving a window where the new contents were readable at the process umask; it is now made private while empty, before the bytes, and removed on every failure path.- calcChain cleanup removed the part but left a namespace-prefixed
(
<r:Relationship>) or single-quoted relationship or content-type override behind; removal now matches every spelling its discovery does. - Header names are canonicalized one way. A schema column with surrounding
space built a table its own read then rejected as
:missing_column, and a padded reserved name (:" id ") slipped past the constructor; validation and indexing now share one trim-and-downcase rule, and a blank column name is refused. - Adding a style to a namespace-prefixed styles part wrote an index the file
could not resolve; such a part is now read-only and the write is refused with
:unsupported, the way a prefixed worksheet already is. - Deleting the last sheet produced a workbook no reader could open; a plan that
would leave no sheets is refused (
:invalid_xlsx), checked on the final state so delete-then-add in one batch still works. - Google whole-sheet reads sent a bare tab name (
Costs), which Google can resolve to a same-named named range in preference to the sheet; whole-sheet reads now quote the name.
0.1.2 (2026-09-18)
Fixes from an external review of 0.1.1. Correctness, and a more honest account of what the concurrency story does and does not cover.
.xlsxreads that lost data. An ISO-date cell (t="d", from a workbook saved withiso_dates) read as nil and, since it read as nothing, was erased on the next write; it now reads as aDateorNaiveDateTime. An elapsed duration ([h]:mm:ss) read as aTime, throwing away whole days (36 hours became 12); it keeps its number and its format now. A float smaller than1.0e-15(such as1.0e-20) was written as0; the shortest round-tripping form is written instead. An error cell (#DIV/0!) with no formula crashed any write to its sheet; the error now reads into the cell's metadata, the same place a formula's error goes, so the write succeeds. (Writing such a cell back verbatim is still to come; an untouched error cell rewrites empty for now.).xlsxwrites that corrupted the file. A shared-string table spelled with a namespace prefix (<x:sst>) silently dropped a newly added string, so the cell read back nil; mutations are namespace-aware now. RemovingcalcChain.xmlleft its relationship and content-type override behind, pointing at a part that was gone; both go with it.- A local write no longer loosens a file's permissions. Replacing a
0600file produced a0644one, because the temporary file took the process umask. The destination's mode is now preserved, a new file is created0600, and the temporary file is created exclusively. Sheetshow.Table.compact/1no longer deletes a live row when a tombstone shares its id after a refresh: an ambiguous tombstone is left for a fresh read to resolve.- Reserved and colliding columns are refused.
Sheetshow.Table.new/2andSheetshow.Log.new/2reject a schema with anidordeletedcolumn, or two columns that differ only by case or spacing; a tab whose header names a needed column twice reads as%Sheetshow.Error{reason: :duplicate_column}rather than silently taking the last. Sheetshow.Schema.cast/2turns a serial number too large to convert into a:casterror, rather than raising.- WebDAV: a
PUTthat returns noETagleaves the version:unknowninstead of adopting one from a follow-upHEAD, which could be a racing writer's. - Google: a batch that adds a tab and then deletes it in one plan no longer leaves the tab in the remembered metadata; adds and deletes are applied in order.
- What Sheetshow can promise, corrected. A conditional-write store makes each
run/2on a file atomic against a racing writer, but does not yet tie aTablewrite to the snapshot it was planned against, becauserun/2re-reads the file at execution. The guide, theSheetshow.Tabledocs and the README said this gap was closed; they now say it is not, and the README's retry note carries the same caveat the guide always has.
0.1.1 (2026-09-17)
Fixes from a QA pass over 0.1.0, all in the .xlsx codec unless said otherwise:
- A column of formulas Excel filled down (one
<f t="shared">and pointers under it) read as empty formulas and was written back as<f></f>, taking the formulas out of the file. Each cell now reads as the shared formula moved to it. - Conditional formatting's fonts and fills (under
<dxfs>) were counted with the lists a cell indexes into, so a new style pointed past the end of<fonts>. - Deleting a sheet whose name a writer had escaped its own way (
Q1's, as LibreOffice does) left the<sheet>element behind, pointing at a part that was gone. - Adding a string rewrote the shared string table from the text alone, flattening rich text in every other cell of the workbook. The table is spliced now.
- A worksheet whose elements carry a namespace prefix (
<x:sheetData>) is read but refused a write, rather than written back with twosheetDataelements. Sheetshow.Table.refresh/2: an id the snapshot had read twice, one of whose rows had since gone, came back as two rows on one sheet row with the flag cleared. Both stay flagged until a fresh read.Sheetshow.records/3raised onheader: false.- A colour such as
"#FF8800\n"passedSheetshow.Style.validate/1and failed in the Google encoder; a sheet name or reference with a trailing newline parsed. Sheetshow.Schema.cast/2: a:jsoncolumn accepts a number or boolean a person typed, and a:booleancolumn accepts1.0and0.0.- Every function that takes options now refuses an option it does not know, as
Sheetshow.plan/2already did: the layout builders,Sheetshow.Client.new/2,Sheetshow.Workbook.xlsx/2,Sheetshow.Token,Sheetshow.Log.Event.new/2,Sheetshow.Table.insert/2anddelete/2,Sheetshow.OAuth,Sheetshow.ServiceAccount.assertion/2,Sheetshow.UserAccount.new/3andSheetshow.Store.webdav/2.
0.1.0 (2026-09-16)
The first release. What is in it:
- Cells and layout.
Sheetshow.Cell,Sheetshow.Coord,Sheetshow.RangeandSheetshow.A1, giving 0-indexed coordinates, inclusive ranges, quoted sheet names and open-ended ranges, withSheetshow.Value(numbers, text, booleans, formulas, dates and times as serial numbers) andSheetshow.Style(a plain map). Layout onSheetshow:row,col,rows,records,to_rows,stack,beside,pad_below,pad_right,shift,put_sheet,put_style,max_rowandmax_col. - Plans.
Sheetshow.plan/2turns cells into backend-neutral ops (AddSheet,DeleteSheet,PutCells,AppendRows,DeleteRows,SetDimensions), onePutCellsper run so a write never clears a neighbour, andSheetshow.run/2carries a plan out in one request.Sheetshow.read_cells/2reads cells with everything about them;read_rows/2reads computed values, one or several ranges in one request. - Backends.
Sheetshow.Workbookover Google Sheets (Sheetshow.Google, OTP's:httpc, TLS verified explicitly, the quota as:rate_limited), over an in-memory spreadsheet that doubles as the test double, and over.xlsxfiles, read one tab at a time and written back with every untouched part copied across compressed, on a local file or a WebDAV server, whereIf-Matchmakes a write conditional.Sheetshow.Backend.capabilities/1says what each promises. - A database on a tab.
Sheetshow.Schemaas a keyword list;Sheetshow.Log, an append-only tab whose state is the fold over its rows, with a cursor read and a view tab;Sheetshow.Table, mutable rows found again by id, with soft delete,refresh/2and a planner that orders hard deletes bottom-up;Sheetshow.ULIDfor client-generated ids. - Credentials.
Sheetshow.ServiceAccountandSheetshow.UserAccount, both reduced to oneSheetshow.Token;Sheetshow.OAuthbuilds the consent URL and reads the answer, with PKCE,access_type=offlineandprompt=consentas the defaults. - Guides that run. Two quick starts, against Google and against an
.xlsxfile; setting up Google; a cookbook; the case for Sheets instead of Postgres; and what Sheetshow can promise. Everyelixirblock in them is executed by the test suite. A cheatsheet generated from the compiled modules, andusage-rules.mdfor an agent's context. - No dependencies, no processes, no macros.