Repository navigation
## v1.28.0 (2026-10-11)
v1.28.0 (2026-10-11)
The entity case-change wave (issue #6)
Third live occurrence of the same trap in the library (ABRAMS/Abrams on
2026-10-04, then Daw Books/DAW Books and ROC/Roc on 2026-10-11, the ROC
row carrying 40 accumulated books): a case-variant target spelling
resolved NOCASE to the existing row and the setter reported already-so,
silently no-oping the consolidation. Issue #6 offered two shapes; the fix
takes the parity one, because the spec's documented rationale turned out
to rest on a misreading of upstream.
- The spec premise was wrong, and said so plainly. §5 deviation 12
(2026-09-12) claimed upstream's write default reuses a case-different
entity row as-is (allow_case_change=False), citingget_db_id. The
flag flows frommany_one/many_many, whose callers default it True
(Writer.set_booksdb/write.py:558,set_fieldcache.py:1970 -- the
path Calibre's GUI metadata edits ride); only the retired legacy
db.pyAPI wrapped False, andchange_casethen rewrites the row's
name and dirties every linked book. The rewritten deviation records
the correction in place, in the same class as the two upstream-factual
fixes the v1.25.0 audit landed. set_publisherconsolidates case variants (the issue's headline).
set_publisher(id, "DAW Books")on aDaw Booksrow now rewrites the
row's name to the requested spelling and touches + queues every book
linked to it, returning True.set_seriesis the same shape, indexes
untouched by an in-place re-case.- Authors get the full upstream treatment.
set_authors(and every
path that funnels through it:add_book, the trash restore) compares
and stores the display form and updates each linked book's
author_sortonly when it matched the string recomputed from the
stored sorts modulo case -- upstream's strcmp rule, so a hand-tuned
sort on an unrelated book survives a spelling fix made from another
book. The row's ownsortis deliberately NOT rewritten: upstream's
change_casewrites only the name column (its asort_map recompute is
session memory that reverts on reopen), so the stored sort -- and any
hand-tuning in it -- survives, withset_author_sort_nameremaining
the re-keying verb. Paths re-lay through the usual deferred machinery. - The tag surface stops fragmenting.
add_tagused to resolve the
exact spelling only and INSERT a duplicate row for a case variant --
the only cquarry path that could create case-duplicate rows. It now
resolves NOCASE and re-cases the shared row (every linked book
touched), andremove_taggained the NOCASE fallback so removing
rocreaches aROCrow instead of honestly failing against the
case. - Custom-column text family included, exclusions named. Pattern-A
text/series/multi-text value rows re-case like the builtin entities
(upstream writes them through the samemany_one/many_many), and
multi-valued items dedupe case-insensitively keeping the first
spelling (upstream'suniq(vals, kmap)), so["ya", "YA"]stores one
row. Deliberately excluded: custom enumerations (upstream's enum
writer hardcodes case changes off), languages (upstream keys them
exactly; odd-case rows reuse as-is), ratings (numeric), and the
rename/removal verbs, whose exact-spelling-first resolution is the
ratified 1.19 contract --rename_entityremains the consolidation
tool, including for pre-existing case-duplicate rows cquarry
deliberately does not heal (fix_case_duplicatesstays Calibre's). - Eleven new regression tests in
TestCaseChangeParity(the DAW
scenario, series indexes, the author strcmp rule, tag
re-case/already-linked/removal, custom-column set/append/dedupe,
language reuse, batch rollback), plus the three tests that pinned the
old behavior re-pinned to the new contract.