feat(schema): subtitle/explicit columns, authors+publishers arrays, float series numbers

Phase 1 of the series/collections/metadata overhaul
(~/.opencode/plan/series-collections-metadata-overhaul.md).

Schema (in place, idempotent):
- media_items.series_number INTEGER -> DOUBLE PRECISION: real-world
  ordinals are decimals (0.5, 1.5 novellas); INTEGER silently truncated
  them (calibre 2.5 pinned as 2 in tests).
- + subtitle TEXT, explicit BOOLEAN (sidecar ingest; ABS explicit /
  Audible isAdult).
- + authors/authors_search/publishers/publishers_search TEXT[] NOT NULL
  (narrators A2a precedent). Arrays are write truth; author/publisher
  varchars become trigger-regenerated display strings
  (sync_author_publisher_display) so every varchar consumer (trgm
  search, facets, API, OPDS/Kobo) keeps working unchanged.
- Row-level NULL normalization for the new arrays in
  set_library_type_name (A2 lesson: Go nil -> explicit NULL -> 23502).
- Backfills ARRAY[author]/ARRAY[publisher] — deliberately no
  comma-splitting ('Tolkien, J.R.R.' would corrupt; scanner kept only
  authors[0] historically).
- collection_items.position INTEGER + (collection_id, position) index
  for playlist ordering.

Go: sqlc regen; MediaMetadata/handler params gain Subtitle/Explicit/
Authors/Publishers (legacy singles still accepted, arrays win);
SeriesNumber int32 -> float64 end to end; metadata_overrides adapted
with legacy override keys ('author'/'publisher') preserved so existing
user overrides survive; bulk author edit writes the array.

Pins widened (docs/contract_pins.md): raw sqlc row key set + the six
new columns; changelog entry added.

Verified: unit suite green; integration sweep (bulk/smart collections/
scanner/stats/sync/queue/calibre/pins/audiobooks) green against the
shared dev DB after in-place migration. Pre-existing unrelated red:
TestOwnershipAndAdminTestSuite/TestAdminCreateUser timezone inheritance
fails identically at the pre-change commit (live shared state).
This commit is contained in:
John O'Keefe committed 2026-10-19 14:17:50 -04:00
1 parent f7dc8189a1
commit 646477afad
30 files changed
+790 -247

No files matched your search

+1 -1
View File
@@ -108,7 +108,7 @@ func TestCalibreLibraryScan(t *testing.T) {
require.Equal(t, "Test Book", book.Title, "Title should match sidecar")
require.Equal(t, "Test Author", book.Author.String, "Author should match sidecar")
require.Equal(t, "Test Series", book.Series.String, "Series should match sidecar")
require.Equal(t, int32(1), book.SeriesNumber.Int32, "Series number should match sidecar")
require.Equal(t, float64(1), book.SeriesNumber.Float64, "Series number should match sidecar")
require.Equal(t, "Test Publisher", book.Publisher.String, "Publisher should match sidecar")
require.Len(t, book.Tags, 2, "Should have 2 tags from sidecar")
require.Contains(t, book.Tags, "Fantasy", "Should have Fantasy tag")
+1 -1
View File
@@ -382,7 +382,7 @@ func createPreviewTestMediaItems(t *testing.T, db *database.Queries, libraryID s
media, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Title: "Preview Test Book " + string(rune('A'+i)),
Author: pgtype.Text{String: "Test Author " + string(rune('A'+i)), Valid: true},
Authors: []string{"Test Author " + string(rune('A'+i))},
Genre: pgtype.Text{String: genre, Valid: true},
FilePath: "/tmp/test" + string(rune('A'+i)) + ".epub",
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
+1 -1
View File
@@ -111,7 +111,7 @@ func smartCreateLibraryAndBooks(t *testing.T, setup *TestServerSetup, userID uui
media, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Title: title,
Author: pgtype.Text{String: "Smart Rule Author", Valid: true},
Authors: []string{"Smart Rule Author"},
Genre: pgtype.Text{String: genre, Valid: true},
FilePath: fmt.Sprintf("/tmp/%s-%d.epub", title, time.Now().UnixNano()),
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
+6 -2
View File
@@ -123,7 +123,7 @@ func createContractFixture(t *testing.T, setup *TestServerSetup) contractFixture
media, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Title: itemTitle,
Author: pgtype.Text{String: "Test Contract Author", Valid: true},
Authors: []string{"Test Contract Author"},
Series: pgtype.Text{String: seriesName, Valid: true},
Genre: pgtype.Text{String: "Science Fiction", Valid: true},
FilePath: "/tmp/contract-test-item.epub",
@@ -711,10 +711,14 @@ func TestContractPin_MediaListBothBranches(t *testing.T) {
fx := createContractFixture(t, setup)
// The sqlc row key sets behind BOTH branches.
// A2b (2026-10-19): + subtitle, explicit, authors, authors_search,
// publishers, publishers_search (docs/contract_pins.md changelog).
rawRowKeys := []string{
"id", "library_id", "title", "author", "isbn", "description",
"id", "library_id", "title", "subtitle", "author", "isbn", "description",
"file_path", "file_size", "mime_type", "cover_image_path",
"series", "series_number", "tags", "narrators", "abridged",
"explicit", "authors", "authors_search", "publishers",
"publishers_search",
"asin", "date_published", "publisher", "contributors", "language",
"edition", "page_count", "genre", "copyright_year", "goodreads_id",
"openlibrary_id", "google_books_id", "added_by_admin_id", "created_at",
+1 -1
View File
@@ -88,7 +88,7 @@ func (s *OwnershipAndAdminTestSuite) createItemViaDB(withFolder bool) (itemID, l
item, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: library.ID,
Title: "Ownership Suite Item",
Author: pgtype.Text{String: "Suite Author", Valid: true},
Authors: []string{"Suite Author"},
FilePath: relPath,
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
})
+1 -1
View File
@@ -51,7 +51,7 @@ func createTestQueueItem(t *testing.T, db *database.Queries, userID uuid.UUID) (
mediaItem, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: library.ID,
Title: "Test Book for Queue",
Author: pgtype.Text{String: "Test Author", Valid: true},
Authors: []string{"Test Author"},
FilePath: "/tmp/test.epub",
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
+1 -1
View File
@@ -544,7 +544,7 @@ func TestLibraryUsageStats(t *testing.T) {
media, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: pgtype.UUID{Bytes: [16]byte(lib), Valid: true},
Title: title,
Author: pgtype.Text{String: author, Valid: true},
Authors: []string{author},
Genre: pgtype.Text{String: genre, Valid: true},
FilePath: fmt.Sprintf("/tmp/usage-%s-%d.bin", title, time.Now().UnixNano()),
FileSize: pgtype.Int8{Int64: size, Valid: true},
+1 -1
View File
@@ -95,7 +95,7 @@ func createSyncTestMedia(t *testing.T, db *database.Queries, libraryID pgtype.UU
media, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
LibraryID: libraryID,
Title: "Sync Test Book",
Author: pgtype.Text{String: "Test Author", Valid: true},
Authors: []string{"Test Author"},
FilePath: "/tmp/test.epub",
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},