diff --git a/.planning/phases/05-data-layer-full-fidelity/05-02-SUMMARY.md b/.planning/phases/05-data-layer-full-fidelity/05-02-SUMMARY.md new file mode 100644 index 0000000..b736571 --- /dev/null +++ b/.planning/phases/05-data-layer-full-fidelity/05-02-SUMMARY.md @@ -0,0 +1,197 @@ +--- +phase: 05-data-layer-full-fidelity +plan: 02 +subsystem: database +tags: [gorm, gormigrate, jsonable, money, validation, pivot, lifecycle, fillable] + +requires: + - phase: 05-data-layer-full-fidelity + provides: Winter-directory registries, lagoon.Fill, WithSoftDeleteCascade, RegisterJoinTable +provides: + - six fonoteka album-slice migrations with idempotent Various Artists seed + - widened Album/Collection plus Artist, Style, AlbumArtist, AlbumRating, AlbumReservation + - lagoon.Jsonable[T] TEXT cast preserving SQL NULL vs empty array + - MoneyString 4-decimal numeric cast with no binary floating type + - lagoon.Validate Laravel rule-string engine with unique:table soft-delete scoping + - Artist/Genre/Style/Collection lifecycle hooks and Album.BeforeSave + - AlbumWriteService/CollectionWriteService fill boundaries and syncArtists +affects: [05-03-secrets, 05-04-attachments, 05-05-remaining-models, 05-06-tests] + +tech-stack: + added: + - github.com/go-playground/validator/v10 v10.30.4 + patterns: + - Jsonable struct with gorm-ignored inner fields so GORM uses Scanner/Valuer + - pivot writes via delete-then-bulk-insert, never Association().Append + - KeepMarketPriceSource gorm:"-" flag threads source provenance into BeforeSave + +key-files: + created: + - lagoon/jsonable.go + - lagoon/jsonable_test.go + - lagoon/validate.go + - lagoon/validate_test.go + - plugins/golem15/fonoteka/updates/10_album_slice.go + - plugins/golem15/fonoteka/models/artist.go + - plugins/golem15/fonoteka/models/style.go + - plugins/golem15/fonoteka/models/album_artist.go + - plugins/golem15/fonoteka/models/album_rating.go + - plugins/golem15/fonoteka/models/album_reservation.go + - plugins/golem15/fonoteka/models/money_string.go + - plugins/golem15/fonoteka/models/slug.go + - plugins/golem15/fonoteka/classes/album_write_service.go + - plugins/golem15/fonoteka/classes/collection_write_service.go + - plugins/golem15/fonoteka/classes/artist_resolver.go + - plugins/golem15/fonoteka/classes/join_tables.go + - plugins/golem15/fonoteka/classes/serialize.go + modified: + - lagoon/fill.go + - plugins/golem15/fonoteka/models/album.go + - plugins/golem15/fonoteka/models/collection.go + - plugins/golem15/fonoteka/models/collection_editor.go + - plugins/golem15/fonoteka/models/genre.go + - plugins/golem15/fonoteka/go.mod + - parity/migrate_test.go + +key-decisions: + - "Jsonable payload field is Data, not Value, because driver.Valuer.Value() collides under go vet" + - "KeepMarketPriceSource is a gorm-ignored Album flag set by SaveAlbum when requested contains market_price_source" + - "CollectionFillFields is name+description; PHP has no CollectionWriteService" + - "Various Artists natural key is name_key='various artists' (PHP seed), slug various-artists" + - "go-playground/validator v10.30.4 is the STACK-named rule engine behind lagoon.Validate" + +patterns-established: + - "One Register call per model/migration/class file init(); plugin.go is not edited" + - "Pivot business columns are written with explicit SQL/GORM Create, Preload uses RegisterJoinTable" + - "Money never lives as a binary floating type; range checks use big.Rat" + +requirements-completed: [DATA-03, DATA-04, DATA-05, DATA-06, DATA-07, DATA-09] + +duration: 27min +completed: 2026-09-18 +--- + +# Phase 5 Plan 02: Album/Collection vertical slice Summary + +**Album/Collection slice with six gormigrate steps, Jsonable/MoneyString casts, lagoon.Validate, ordered album_artists pivot writes, and PHP lifecycle hooks proven on real Postgres** + +## Performance + +- **Duration:** 27 min +- **Started:** 2026-09-18T16:55:11Z +- **Completed:** 2026-09-18T17:22:30Z +- **Tasks:** 3 +- **Files modified:** 32 + +## Accomplishments + +- Six Fonoteka migrations widen collections/albums (including `track_titles` and TEXT `discogs_id`) and create artists (Various Artists seed), styles, ratings, and reservations; each rolls back in isolation +- `lagoon.Jsonable[T]` round-trips SQL NULL vs `'[]'`; `MoneyString` formats Postgres `numeric(10,4)` to a 4-decimal string with no binary floating type in the cast +- Artist/Genre/Style `BeforeValidate`/`AfterDelete` and Collection `BeforeDelete` cascade (via `WithSoftDeleteCascade`) are ported from PHP and tested against real Postgres +- `lagoon.Validate` translates this slice's rule strings (`between`, quoted `oneof`, custom money range, `unique:table` with `deleted_at`) and fails loudly on unknown tokens +- `SaveAlbum` uses the PHP `FILL_FIELDS` subset, `syncArtists` delete-then-insert, and `Album.BeforeSave` (`refreshTrackTitles` + `stampMarketPrice`) including the PHP ceiling case `"12.55555"` → `"12.5556"` + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Album/collection slice migrations** - `e24a614` (feat) in fonoteka.go +2. **Task 2: lagoon.Jsonable** - `20b0199` (feat) in summercms.go +3. **Task 2: Widen models and port lifecycle hooks** - `445133e` (feat) in fonoteka.go +4. **Task 3: lagoon.Validate** - `2c5d92d` (feat) in summercms.go +5. **Task 3: Write services and BeforeSave** - `4879996` (feat) in fonoteka.go + +**Plan metadata:** pending (this file) + +## Files Created/Modified + +- `lagoon/jsonable.go`, `lagoon/jsonable_test.go` — TEXT JSON cast, NULL vs `[]` +- `lagoon/validate.go`, `lagoon/validate_test.go` — rule-string engine +- `lagoon/fill.go` — nil zeros the field; jsonable columns Scan from JSON +- `fonoteka.go/plugins/golem15/fonoteka/updates/10_album_slice.go` — six migrations +- `fonoteka.go/plugins/golem15/fonoteka/models/album.go` — final columns, Fillable/Rules, BeforeSave +- `fonoteka.go/plugins/golem15/fonoteka/models/{artist,style,album_artist,album_rating,album_reservation,money_string,slug}.go` +- `fonoteka.go/plugins/golem15/fonoteka/models/{collection,collection_editor,genre}.go` — widened + hooks +- `fonoteka.go/plugins/golem15/fonoteka/classes/{album_write_service,collection_write_service,artist_resolver,join_tables,serialize}.go` +- `fonoteka.go/parity/migrate_test.go`, `album_slice_models_test.go`, `album_write_service_test.go` + +## Decisions Made + +- Jsonable payload is `Data` (not `Value`) because `driver.Valuer.Value()` plus a field named `Value` fails `go vet` +- `KeepMarketPriceSource` is a `gorm:"-"` flag on Album, set by `SaveAlbum` when `requested` contains `market_price_source` +- `CollectionFillFields` is `name`, `description` — PHP has no CollectionWriteService, so the service list is a subset of `Fillable()` excluding `owner_id` +- Various Artists seed uses PHP's natural key `name_key = "various artists"` (slug `various-artists`) +- `github.com/go-playground/validator/v10 v10.30.4` is added to the framework module as the STACK-named engine behind `lagoon.Validate` + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 3 - Blocking] Jsonable cannot use a field named Value** +- **Found during:** Task 2 (Jsonable cast) +- **Issue:** `go vet` rejects a struct field and method both named `Value`; `type Jsonable[T] T` is also illegal in Go +- **Fix:** Struct with `Data`/`Valid`/`NullOnEmpty` tagged `gorm:"-"` so GORM uses Scanner/Valuer instead of walking inner fields +- **Files modified:** `lagoon/jsonable.go`, `lagoon/jsonable_test.go` +- **Verification:** `go vet ./lagoon/...` and `go test ./lagoon/... -run TestJsonable` +- **Committed in:** `20b0199` (Task 2) + +**2. [Rule 2 - Missing Critical] Fill ignored nil on non-pointer fields** +- **Found during:** Task 3 (SaveAlbum money nil-in → NULL-out) +- **Issue:** `lagoon.Fill` left `MoneyString` unchanged on explicit nil, so the blank-to-NULL path could not be requested +- **Fix:** nil now zeros any field; types implementing `sql.Scanner` (Jsonable) Scan from JSON when convertValue cannot assign +- **Files modified:** `lagoon/fill.go` +- **Verification:** `TestSaveAlbumMoneyNormalization` nil case +- **Committed in:** `2c5d92d` (Task 3) + +**3. [Rule 3 - Blocking] Existing migrate tests hardcoded two Fonoteka IDs** +- **Found during:** Task 1 (six new migrations) +- **Issue:** `TestMigrateSeedsCanonicalGenres` and `TestRollbackLastIsolatesFonoteka` expected exactly the Phase-3 pair +- **Fix:** Expect eight IDs; RollbackLast now asserts `create_album_reservations` drops only that table +- **Files modified:** `fonoteka.go/parity/migrate_test.go` +- **Verification:** those tests plus `TestAlbumSliceMigrationsUpDown` +- **Committed in:** `e24a614` (Task 1) + +**4. [Rule 2 - Missing Critical] widen_albums/collections needed columns omitted from Phase 3 stubs** +- **Found during:** Task 1 (reading folded PHP files) +- **Issue:** `description` (collections), `public_enabled`/`public_token_generated_at`, and `shelf`/`quantity`/`notes` exist in PHP but not in `00_base.go` +- **Fix:** Added every column the folded PHP files (and the RESEARCH fillable list) require +- **Files modified:** `fonoteka.go/plugins/golem15/fonoteka/updates/10_album_slice.go` +- **Verification:** `TestAlbumSliceMigrationsUpDown` and later model tests +- **Committed in:** `e24a614` (Task 1) + +--- + +**Total deviations:** 4 auto-fixed (2 missing critical, 2 blocking) +**Impact on plan:** Required for vet, money nil-out, and a compiling schema that matches PHP. No scope creep. Album.Fillable() has 21 names copied from PHP/RESEARCH (plan said 20 — a count error). + +## Issues Encountered + +None beyond the auto-fixes above. + +## Authentication Gates + +None. + +## Known Stubs + +- `classes/artist_resolver.go` `albumBeforeSaveCallback` is a type-guard registration seam; name-to-id find-or-create stays in PHP's `ArtistResolver::resolveOne` and is not needed for this plan's ID-based `syncArtists` +- `SerializeAlbum`/`SerializeCollection` omit photos/thumbs (Plan 05-04) and reservation context (Phase 12) + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +Ready for 05-03 (secrets/encrypted credentials). Wave-2 Album/Collection models, validations, and write-service fill boundaries are in place. `lagoon.Validate` and `lagoon.Jsonable` are reusable for later models. + +## Self-Check: PASSED + +- Key files exist on disk (jsonable, validate, 10_album_slice, write services, join_tables, models) +- Commits `e24a614`, `20b0199`, `445133e`, `2c5d92d`, `4879996` exist +- `go vet ./...` and `go test ./lagoon/...` green in summercms.go +- `go vet ./...` and `go test ./...` green in fonoteka.go (parity + plugin modules) + +--- +*Phase: 05-data-layer-full-fidelity* +*Completed: 2026-09-18*