Current section
Files
Jump to
Current section
Files
CHANGELOG.md
# Changelog
Pre-1.0: a minor version may rename or remove. When it does, the migration is one
line here.
## 0.1.4 (2026-09-19)
A third external review pass over 0.1.3, correctness and safety again, most of it
in the `.xlsx` codec.
- **Adding a sheet could redirect an existing one to an empty part.** A workbook
that quoted its relationship attributes with `'` read fine, but the next
relationship id was allocated by a `Id="rId..."` regex that saw none of them, so
a new sheet claimed `rId1` on top of one already in use and the old tab resolved
to the new, empty worksheet. The id, and the next sheet id beside it, are read
through the parser now, so the same file the reader accepts is the file they
allocate against.
- **calcChain cleanup missed a valid spelling.** Removing the part left its
relationship and content-type override behind when the declaration used
whitespace around its `=` or a separate closing tag rather than a self-closing
one. Removal is one spelling-agnostic pass now, tolerant of a namespace prefix,
either quote style, that whitespace and either closing form, and the same pass
removes a deleted sheet's `<sheet>` element.
- **A namespace-prefixed styles part was only half-detected.** The guard checked
the root `<styleSheet>` alone, so a file whose root was unprefixed but whose
`<fonts>`, `<fills>` or `<cellXfs>` lists were prefixed slipped through and had a
second, unprefixed list appended with an index no cell could resolve. The lists
are checked too now, and such a part is read-only, refused with `:unsupported`.
- **A shared or inline string that looked like an escape read back wrong.** The
SpreadsheetML `_xHHHH_` escaping (a control character, or a literal underscore
written `_x005F_`) is not XML escaping and the parser does not undo it; a literal
`_x0041_` came back as the encoded `_x005F_x0041_` and a carriage return as
`_x000D_`. It is decoded on read, in one pass so `_x005F_x0041_` is the literal
`_x0041_` and not `A`, and encoded to match on write.
- **A values read over a file ignored a formula's cached result.** `read_rows/2`
returned the formula rather than the value a spreadsheet cached beside it, so a
`Table` or `Log` field over `.xlsx` cast the formula and came back nil. It gives
the computed value now, the way Google's values read does, with a cached error as
its text; `read_cells/2` still returns the formula, with the result in its
metadata.
- **A number cast into a text column lost precision or raised.** Fixed 15 decimals
rounded a value just above `1.0e-15` to fewer digits than it had, and raised
outright on a magnitude like `1.0e308`. The shortest round-tripping form is
written whenever the fixed one would not read back the same, so every finite
float round-trips and the lenient cast stays lenient.
- **A log cursor read truncated columns a whole read kept.** The header and the
rows from the cursor were both bounded to the schema's width, so a column added
to the tab by hand pushed a real column out of range and the read failed with
`:missing_column` where a whole read found it by name. The header is read whole
now, and a tab wider than its schema costs a second request rather than dropping
the columns.
## 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
number `1.5` rather than a 12-hour `Time`.
- **An ISO time-only cell (`t="d"` holding `12:30:00`, from `iso_dates`) read as nil**
and was erased on the next write; it now reads as a `Time`.
- **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.Local`** created 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.
- **`.xlsx` reads that lost data.** An ISO-date cell (`t="d"`, from a workbook
saved with `iso_dates`) read as nil and, since it read as nothing, was erased on
the next write; it now reads as a `Date` or `NaiveDateTime`. An elapsed duration
(`[h]:mm:ss`) read as a `Time`, throwing away whole days (36 hours became 12); it
keeps its number and its format now. A float smaller than `1.0e-15` (such as
`1.0e-20`) was written as `0`; 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.)
- **`.xlsx` writes 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. Removing `calcChain.xml` left
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 `0600`
file produced a `0644` one, because the temporary file took the process umask.
The destination's mode is now preserved, a new file is created `0600`, and the
temporary file is created exclusively.
- **`Sheetshow.Table.compact/1`** no 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/2` and
`Sheetshow.Log.new/2` reject a schema with an `id` or `deleted` column, 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/2`** turns a serial number too large to convert into a
`:cast` error, rather than raising.
- **WebDAV**: a `PUT` that returns no `ETag` leaves the version `:unknown` instead
of adopting one from a follow-up `HEAD`, 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/2` on a file atomic against a racing writer, but does not yet tie a `Table`
write to the snapshot it was planned against, because `run/2` re-reads the file
at execution. The guide, the `Sheetshow.Table` docs 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 two `sheetData` elements.
- `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/3` raised on `header: false`.
- A colour such as `"#FF8800\n"` passed `Sheetshow.Style.validate/1` and failed in
the Google encoder; a sheet name or reference with a trailing newline parsed.
- `Sheetshow.Schema.cast/2`: a `:json` column accepts a number or boolean a
person typed, and a `:boolean` column accepts `1.0` and `0.0`.
- Every function that takes options now refuses an option it does not know, as
`Sheetshow.plan/2` already did: the layout builders, `Sheetshow.Client.new/2`,
`Sheetshow.Workbook.xlsx/2`, `Sheetshow.Token`, `Sheetshow.Log.Event.new/2`,
`Sheetshow.Table.insert/2` and `delete/2`, `Sheetshow.OAuth`,
`Sheetshow.ServiceAccount.assertion/2`, `Sheetshow.UserAccount.new/3` and
`Sheetshow.Store.webdav/2`.
## 0.1.0 (2026-09-16)
The first release. What is in it:
- **Cells and layout.** `Sheetshow.Cell`, `Sheetshow.Coord`, `Sheetshow.Range` and
`Sheetshow.A1`, giving 0-indexed coordinates, inclusive ranges, quoted sheet
names and open-ended ranges, with `Sheetshow.Value` (numbers, text, booleans,
formulas, dates and times as serial numbers) and `Sheetshow.Style` (a plain
map). Layout on `Sheetshow`: `row`, `col`, `rows`, `records`,
`to_rows`, `stack`, `beside`, `pad_below`, `pad_right`, `shift`, `put_sheet`,
`put_style`, `max_row` and `max_col`.
- **Plans.** `Sheetshow.plan/2` turns cells into backend-neutral ops (`AddSheet`,
`DeleteSheet`, `PutCells`, `AppendRows`, `DeleteRows`, `SetDimensions`), one
`PutCells` per run so a write never clears a neighbour, and `Sheetshow.run/2`
carries a plan out in one request. `Sheetshow.read_cells/2` reads cells with
everything about them; `read_rows/2` reads computed values, one or several
ranges in one request.
- **Backends.** `Sheetshow.Workbook` over 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 `.xlsx` files,
read one tab at a time and written back with every untouched part copied across
compressed, on a local file or a WebDAV server, where `If-Match` makes a write
conditional. `Sheetshow.Backend.capabilities/1` says what each promises.
- **A database on a tab.** `Sheetshow.Schema` as 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/2` and a planner that orders hard deletes bottom-up; `Sheetshow.ULID`
for client-generated ids.
- **Credentials.** `Sheetshow.ServiceAccount` and `Sheetshow.UserAccount`, both
reduced to one `Sheetshow.Token`; `Sheetshow.OAuth` builds the consent URL and
reads the answer, with PKCE, `access_type=offline` and `prompt=consent` as the
defaults.
- **Guides that run.** Two quick starts, against Google and against an `.xlsx`
file; setting up Google; a cookbook; the case for Sheets instead of Postgres;
and what Sheetshow can promise. Every `elixir` block in them is executed by the
test suite. A cheatsheet generated from the compiled modules, and
`usage-rules.md` for an agent's context.
- No dependencies, no processes, no macros.