198 lines
10 KiB
Markdown
198 lines
10 KiB
Markdown
---
|
|
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*
|