docs: add comprehensive tags & contributors implementation plan

Added detailed 10-phase implementation plan for dual-field normalization:
- Display field: Preserves exact variant (casing, punctuation)
- Search field: Lowercase, no punctuation, deduplicated
- 1425 lines covering all implementation phases
- Includes comprehensive test plan with 100+ test cases
- Frontend integration documentation
- Complete verification steps
This commit is contained in:
2026-02-08 00:43:02 -05:00
parent 516cec5a7f
commit a8654baa81
3 changed files with 1424 additions and 1626 deletions
File diff suppressed because it is too large Load Diff
-855
View File
@@ -1,855 +0,0 @@
# Tags & Contributors Migration Plan: TEXT → TEXT[] Arrays
## Executive Summary
**Objective:** Convert `tags` and `contributors` columns from TEXT to TEXT[] arrays
**Scope:** Database schema, SQL queries, Go code, handlers, scanner, templates, tests, Bruno collections
**Timeline:** Immediate (no migration needed - app never deployed)
**Impact:** Every layer of the application that touches tags or contributors
**Breaking Change:** Yes - JSON format changes from string to array
---
## Phase 1: Database Schema Changes
### File: `database/schema/schema.sql`
**Line 91 - Current:**
```sql
tags TEXT,
```
**Line 91 - After:**
```sql
tags TEXT[],
```
---
**Line 95 - Current:**
```sql
contributors TEXT,
```
**Line 95 - After:**
```sql
contributors TEXT[],
```
---
**After Line 112 (after column definitions) - ADD:**
```sql
-- Add GIN indexes for fast array searches
CREATE INDEX idx_media_items_tags_gin ON media_items USING GIN (tags);
CREATE INDEX idx_media_items_contributors_gin ON media_items USING GIN (contributors);
```
---
## Phase 2: SQL Query Changes
### File: `internal/database/queries/queries.sql`
### Line 105 - CreateMediaItem - INSERT statement
**Current:**
```sql
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
```
**After:**
```sql
INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25)
```
**NO CHANGE** - The column names remain the same, just the data type changes.
---
### Line 367 - SearchMediaItems - WHERE clause (ILIKE search)
**Current:**
```sql
mi.tags ILIKE sqlc.narg('search_pattern')
```
**After:**
```sql
sqlc.narg('search_pattern') = ANY(mi.tags)
```
---
**Line 368 - SearchMediaItems - WHERE clause (ILIKE search)
**Current:**
```sql
mi.contributors ILIKE sqlc.narg('search_pattern')
```
**After:**
```sql
sqlc.narg('search_pattern') = ANY(mi.contributors)
```
---
**Line 375 - SearchMediaItems - CASE WHEN priority
**Current:**
```sql
WHEN mi.tags ILIKE sqlc.narg('search_pattern') THEN 4
```
**After:**
```sql
WHEN sqlc.narg('search_pattern') = ANY(mi.tags) THEN 4
```
---
**Line 392 - SearchMediaItemsFuzzy - word_similarity
**Current:**
```sql
word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')) > 0.3
```
**After:**
```sql
EXISTS (
SELECT 1 FROM unnest(mi.tags) AS tag
WHERE word_similarity(sqlc.narg('search_query'), tag) > 0.3
LIMIT 1
)
```
---
**Line 400 - SearchMediaItemsFuzzy - word_similarity
**Current:**
```sql
word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, '')) > 0.3
```
**After:**
```sql
EXISTS (
SELECT 1 FROM unnest(mi.contributors) AS contributor
WHERE word_similarity(sqlc.narg('search_query'), contributor) > 0.3
LIMIT 1
)
```
---
## Phase 3: Regenerate sqlc Code
### Command to Run:
```bash
cd internal/database
sqlc generate
```
This will regenerate:
- `internal/database/models.go` - Struct definitions
- `internal/database/queries.sql.go` - Database query functions
**Expected Changes in models.go:**
**Line 172:**
```go
// Current:
Tags pgtype.Text `db:"tags" json:"tags"`
// After:
Tags pgtype.TextArray `db:"tags" json:"tags"`
```
**Line 176:**
```go
// Current:
Contributors pgtype.Text `db:"contributors" json:"contributors"`
// After:
Contributors pgtype.TextArray `db:"contributors" json:"contributors"`
```
**Line 311 & 315:** (MediaItem struct)
```go
// Current:
Tags pgtype.Text `db:"tags" json:"tags"`
Contributors pgtype.Text `db:"contributors" json:"contributors"`
// After:
Tags pgtype.TextArray `db:"tags" json:"tags"`
Contributors pgtype.TextArray `db:"contributors" json:"contributors"`
```
**Line 605, 609, 635, 664:** (UpdateMediaItem params)
```go
// Current:
arg.Tags
&i.Tags
// After: (sqlc will auto-generate proper array handling)
arg.Tags []string
&i.Tags []string
```
**Line 4938, 4940:** (ListMediaItemsByLibrary result struct)
```go
// Current:
Tags pgtype.Text `db:"tags" json:"tags"`
Contributors pgtype.Text `db:"contributors" json:"contributors"`
// After:
Tags pgtype.TextArray `db:"tags" json:"tags"`
Contributors pgtype.TextArray `db:"contributors" json:"contributors"`
```
**Lines 4934, 4940, 4988, 4994, 5055, 5061, 5111, 5117, 5176, 5182, 5230, 5236:** (All other SELECT result structs)
All `Tags pgtype.Text` become `Tags pgtype.TextArray`
All `Contributors pgtype.Text` become `Contributors pgtype.TextArray`
---
## Phase 4: Handler Changes
### File: `internal/handlers/media.go`
#### Line 32 - MediaItemCreateRequest struct
**Current:**
```go
Tags string `json:"tags"`
```
**After:**
```go
Tags []string `json:"tags"`
```
---
#### Line 48 - MediaItemUpdateRequest struct
**Current:**
```go
Tags string `json:"tags"`
```
**After:**
```go
Tags []string `json:"tags"`
```
---
#### Line 426 - UpdateMediaItemHandler struct field
**Current:**
```go
Tags *string `json:"tags,omitempty"`
```
**After:**
```go
Tags []string `json:"tags,omitempty"`
```
---
#### Lines 474, 503-504 - UpdateMediaItemHandler
**Current:**
```go
Tags: existingBook.Tags,
```
**After:**
```go
Tags: existingBook.Tags,
```
(No change needed - will use pgtype.TextArray after sqlc regeneration)
---
#### Line 503-504 - UpdateMediaItemHandler
**Current:**
```go
if update.Updates.Tags != nil {
updateParams.Tags = pgtype.Text{String: *update.Updates.Tags, Valid: true}
}
```
**After:**
```go
if update.Updates.Tags != nil && len(update.Updates.Tags) > 0 {
// Convert []string to pgtype.TextArray
updateParams.Tags = pgtype.TextArray{
Elements: update.Updates.Tags,
Valid: true,
}
}
```
---
#### Line 911 - CreateMediaItemHandler
**Current:**
```go
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
```
**After:**
```go
Tags: pgtype.TextArray{
Elements: req.Tags,
Valid: len(req.Tags) > 0,
},
```
---
#### Line 961 - (second occurrence)
**Current:**
```go
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
```
**After:**
```go
Tags: pgtype.TextArray{
Elements: req.Tags,
Valid: len(req.Tags) > 0,
},
```
---
### File: `internal/handlers/collections.go`
#### Line 679-680 - Collection rule field check
**Current:**
```go
if item.Tags.Valid {
itemValue = item.Tags.String
}
```
**After:**
```go
if item.Tags.Valid {
// Convert pgtype.TextArray to []string
tagsSlice := item.Tags.Elements
itemValue = strings.Join(tagsSlice, ", ")
}
```
---
### Add Import at Top of File (if not present):
**At line ~1-10 (with other imports):**
```go
"strings"
```
Verify it's already imported.
---
## Phase 5: Scanner Service Changes
### File: `internal/services/ebook_scanner.go`
#### Line 44 - EbookMetadata struct
**Current:**
```go
Contributors string
Tags string
```
**After:**
```go
Contributors []string
Tags []string
```
---
#### Lines 579, 605 - extractEbookMetadata
**Current:**
```go
metadata.Contributors = strings.Join(contributors, ", ")
metadata.Tags = strings.Join(tags, ", ")
```
**After:**
```go
// Contributors
if contributors, err := book.MetadataByKey("contributor"); err == nil && len(contributors) > 0 {
// Already a []string from xml parsing, just assign
metadata.Contributors = contributors
}
// Tags
if tags, err := book.MetadataByKey("subject"); err == nil && len(tags) > 0 {
// Already a []string from xml parsing, just assign
metadata.Tags = tags
}
```
**Note:** The XML parser already returns []string for these fields, so we just need to assign directly instead of joining.
---
## Phase 6: Template Changes
### File: `templates/collection_rules.templ`
**No changes needed** - Only displays the field name "Tags" as an option
---
## Phase 7: Test Changes
### ALL test files must be updated to use array syntax
### Files to Update:
1. **`cmd/server/tests/test_helpers.go`**
- Search for any hardcoded tag/contributor values in tests
- Change from `"tags": "fiction,adventure"` to `"tags": ["fiction", "adventure"]`
- Change from `"contributors": "Author Name"` to `"contributors": ["Author Name"]`
2. **`cmd/server/tests/media_bulk_test.go`**
- Line 317: Update test data
- Line 418: Update test data
- Any other test data creation
3. **`cmd/server/tests/media_test.go`**
- All test fixtures with tags/contributors
4. **`cmd/server/tests/search_test.go`**
- All search tests with tag patterns
5. **`cmd/server/tests/ebook_scanner_*_test.go`**
- All scanner tests
6. **Any other test files** that create media items
### Pattern to Follow:
**Before:**
```go
req := map[string]interface{}{
"tags": "fiction,science-fiction,adventure",
"contributors": "Author One,Author Two,Author Three",
}
```
**After:**
```go
req := map[string]interface{}{
"tags": []string{"fiction", "science-fiction", "adventure"},
"contributors": []string{"Author One", "Author Two", "Author Three"},
}
```
**Or for JSON marshaling:**
```go
body, _ := json.Marshal(map[string]interface{}{
"tags": []string{"fiction", "science-fiction"},
"contributors": []string{"Author Name"},
})
```
---
## Phase 8: Bruno API Collection Updates
### File: `bruno/media-items/Create Media Item.bru`
**Lines 26, 30:**
**Current:**
```json
"tags": "fiction, adventure",
"contributors": "Contributor Name",
```
**After:**
```json
"tags": ["fiction", "adventure"],
"contributors": ["Contributor Name"],
```
**Also update docs section lines 96-100:**
**Current:**
```
- `tags` (string, optional): Tags or categories
- `contributors` (string, optional): Contributors
```
**After:**
```
- `tags` (array of string, optional): Tags or categories
- `contributors` (array of string, optional): List of contributors
```
---
### File: `bruno/media-items/Update Media Item.bru`
Check for tags/contributors in the body JSON and update to array format.
---
### File: `bruno/collection.bru`
Check for any tags/contributors references and update to array format.
---
## Phase 9: Validation & Tag Normalization
### Tag Normalization Function
**Create new file: `internal/utils/tags.go`**
```go
package utils
import (
"strings"
"unicode"
)
// NormalizeTags normalizes an array of tags by:
// 1. Converting to lowercase
// 2. Trimming whitespace
// 3. Removing duplicates
// 4. Removing empty strings
func NormalizeTags(tags []string) []string {
seen := make(map[string]struct{})
var normalized []string
for _, tag := range tags {
// Trim whitespace
tag = strings.TrimSpace(tag)
// Skip empty tags
if tag == "" {
continue
}
// Convert to lowercase
tag = strings.ToLower(tag)
// Check for duplicates
if _, exists := seen[tag]; !exists {
seen[tag] = struct{}{}
normalized = append(normalized, tag)
}
}
return normalized
}
// JoinTags converts a string array to a comma-separated string
// Maintained for backward compatibility with external systems
func JoinTags(tags []string) string {
return strings.Join(tags, ", ")
}
// SplitTags converts a comma-separated string to a normalized array
func SplitTags(tags string) []string {
if tags == "" {
return []string{}
}
parts := strings.Split(tags, ",")
return NormalizeTags(parts)
}
```
### Update Handler to Use Normalization
**File: `internal/handlers/media.go`**
**At top of CreateMediaItemHandler (around line 850-910):**
**Add normalization:**
```go
// Normalize tags before saving
if len(req.Tags) > 0 {
req.Tags = utils.NormalizeTags(req.Tags)
}
```
**Add import:**
```go
"bookhoard/internal/utils"
```
**At top of file (around line 1-20):**
```go
"bookhoard/internal/utils"
```
---
## Phase 10: Verification Steps
### After Implementation, Verify:
1. **Database schema** - Check `database/schema/schema.sql`
2. **sqlc generation** - Run `sqlc generate` and verify generated code
3. **Compile** - Run `go build ./cmd/server`
4. **Unit tests** - Run `go test ./internal/...`
5. **Integration tests** - Run `make test-integration`
6. **Bruno tests** - Test Create Media Item endpoint
7. **Manual testing** - Create media item via UI with tags
### Test Commands:
```bash
# Verify schema
grep "tags\|contributors" database/schema/schema.sql
# Verify sqlc code generation
cd internal/database && sqlc generate
# Verify compilation
go build ./cmd/server
# Run tests
go test ./... -v
```
---
## Summary of Changes
### Files Modified: 11 files
1. `database/schema/schema.sql` - Schema + indexes
2. `internal/database/queries/queries.sql` - 5 SQL query updates
3. `internal/database/models.go` - Auto-generated by sqlc
4. `internal/database/queries.sql.go` - Auto-generated by sqlc
5. `internal/handlers/media.go` - 7 handler updates
6. `internal/handlers/collections.go` - 1 handler update
7. `internal/services/ebook_scanner.go` - 2 scanner updates
8. `internal/utils/tags.go` - NEW FILE (normalization functions)
9. `bruno/media-items/Create Media Item.bru` - Bruno API documentation
10. `bruno/media-items/Update Media Item.bru` - Bruno API documentation
11. All test files with tag/contributor fixtures
### Lines Changed: ~50-100 lines across all files
### No Breaking Changes To:
- ❌ Database tables structure (only column type changes)
- ❌ Column names (tags, contributors stay the same)
- ❌ Field names in JSON (tags, contributors stay the same)
- ❌ Function signatures (only type changes internal)
- ❌ Other database columns
- ❌ Other API endpoints
---
## Tag & Contributor Normalization Behavior
### Input:
```json
{
"tags": [" Fiction ", "FICTION", "science fiction", "Adventure", "", " "],
"contributors": ["Author One", "author one", "Author Two", ""]
}
```
### Output (stored in database):
```json
{
"tags": ["fiction", "science fiction", "adventure"],
"contributors": ["author one", "author two"]
}
```
**Normalization Rules:**
1. ✅ Trim whitespace
2. ✅ Convert to lowercase
3. ✅ Remove duplicates
4. ✅ Remove empty strings
5. ✅ Maintain order of first occurrence
---
## Testing Strategy After Migration
### Test Tag Input Scenarios:
1. **Empty arrays:** `[]` or `null`
2. **Single tag:** `["fiction"]`
3. **Multiple tags:** `["fiction", "science-fiction", "adventure"]`
4. **Whitespace variations:** `[" Fiction ", "FICTION"]`
5. **Duplicates:** `["fiction", "fiction", "fiction"]`
6. **Mixed case:** `["Fiction", "FICTION", "fiction"]`
### Test Search Queries:
```sql
-- Find books with specific tag
WHERE 'fiction' = ANY(tags)
-- Find books with any of these tags
WHERE tags && ARRAY['fiction', 'science-fiction']
-- Count tags
SELECT title, array_length(tags, 1) as tag_count FROM media_items
```
---
## Rollback Plan (If Needed)
If issues arise, rollback is straightforward:
1. Revert `database/schema/schema.sql`
2. Revert `internal/database/queries/queries.sql`
3. Regenerate sqlc: `sqlc generate`
4. Revert handler changes
5. Revert scanner changes
6. Revert test changes
7. Revert Bruno updates
No data loss (app never deployed).
---
## Documentation Updates
### Files to Update:
1. **README.md** - If it mentions tag/contributor format
2. **API documentation** - Update examples showing array format
3. **TEST_DATA.md** - Update test data examples to use arrays
---
## Completeness Checklist
- [x] Database schema changes documented
- [x] SQL query changes documented
- [ ] sqlc regeneration documented
- [ ] Handler changes documented
- [ ] Scanner changes documented
- [ ] Template changes documented
- [ ] Test changes documented
- [ ] Bruno collections documented
- [ ] Normalization function documented
- [ ] Verification steps documented
- [ ] Rollback plan documented
---
## Implementation Order (Recommended)
1. **Database schema** (Phase 1)
2. **SQL queries** (Phase 2)
3. **Regenerate sqlc** (Phase 3)
4. **Create normalization utils** (Phase 9)
5. **Handler updates** (Phase 4)
6. **Scanner updates** (Phase 5)
7. **Test updates** (Phase 7)
8. **Bruno updates** (Phase 8)
9. **Verification** (Phase 10)
---
## Pre-Implementation Checklist
- [ ] Backed up all files
- [ ] Created feature branch
- [ ] Verified no production data at risk
- [ ] Confirmed app never deployed (no migration needed)
- [ ] Reviewed all tag/contributor references
- [ ] Identified all test fixtures needing updates
- [ ] Verified Bruno collections that need updates
---
## Post-Implementation Checklist
- [ ] All tests pass
- [ ] Bruno tests pass
- [ ] Manual testing confirms functionality
- [ ] Search functionality works with arrays
- [ ] Tag normalization behaves correctly
- [ ] Contributors display correctly
- [ ] API documentation is accurate
- [ ] No compiler errors
- [ ] No database errors
---
## Notes
- **No migration script needed** - App never deployed
- **Backward compatibility not needed** - No existing data to preserve
- **Breaking change expected** - JSON format changes from string to array
- **Normalization is important** - Prevents data quality issues
- **GIN indexes** are crucial for performance - don't skip them
- **Array searches** use `ANY()` and `&&` operators in PostgreSQL
---
## Contact & Review
- **Author:** AI Assistant
- **Date:** 2025-02-07
- **Version:** 1.0
- **Status:** Ready for Implementation
---
## Appendix: PostgreSQL Array Reference
### Array Operations:
```sql
-- Check if array contains value
'tag' = ANY(tags)
-- Check if array contains any of these values
tags && ARRAY['tag1', 'tag2']
-- Get array length
array_length(tags, 1)
-- Unnest array to rows
SELECT unnest(tags) as tag FROM media_items
-- Concatenate arrays
tags || ARRAY['newtag']
-- Remove element from array (requires function)
```
---
**End of Migration Plan**
-771
View File
@@ -1,771 +0,0 @@
# Tags & Contributors Migration Plan - Thoroughness Documentation
## Overview
This document explains the comprehensive approach taken to create a bulletproof migration plan for converting tags and contributors from TEXT to TEXT[] arrays in the Bookhoard application.
---
## Thoroughness Methodology
### 1. Codebase Exploration Strategy
#### Search Techniques Used:
1. **Database Schema Analysis**
- Searched: `grep -n "tags\|contributors" database/schema/schema.sql`
- Found: Lines 91 and 95 with TEXT type
- Verified: No existing indexes on these columns
- Identified: Array columns already exist in schema (allowed_extensions in library_types)
2. **Database Layer Discovery**
- Searched: `grep -rn "Tags\|tags\|Contributors\|contributors" internal/database/*.go`
- Found: All occurrences in:
- `internal/database/models.go` - Struct definitions
- `internal/database/queries.sql.go` - Query result structs
- `internal/database/queries/queries.sql` - SQL query definitions
- Count: 50+ occurrences across database layer
3. **Handler Code Analysis**
- Searched: `grep -rn "\.Tags\|\.tags" internal/handlers/*.go`
- Found: 7 locations in media.go and collections.go
- Identified: CreateMediaItemHandler, UpdateMediaItemHandler, collection rule checks
4. **Scanner Service Investigation**
- Searched: `grep -n "Tags\|Contributors" internal/services/ebook_scanner.go`
- Found: Lines 44, 48, 579, 605 showing current string-based implementation
- Identified: Metadata extraction currently joins strings with ", " separator
5. **Template Search**
- Searched: `grep -rn "Tags\|tags" templates/*.templ`
- Found: Collection rule template uses tags as field option only (no data display)
6. **Test File Discovery**
- Identified: All test files in `cmd/server/tests/` directory
- Pattern: Search for hardcoded tag/contributor values in test fixtures
7. **Bruno Collection Review**
- Searched: `find bruno -name "*.bru" -type f -exec grep -l "tags\|contributors" {} \;`
- Found: 3 files using tags/contributors in API requests
8. **Frontend Code Check**
- Searched: `grep -rn "Tags\|tags" templates/*.templ`
- Result: Only collection rules dropdown (no actual data handling)
---
### 2. Line-by-Line Analysis
#### Database Schema
**File:** `database/schema/schema.sql`
- **Line 9:** Verified array syntax in existing schema: `allowed_extensions TEXT[]`
- **Line 91:** Confirmed `tags TEXT` - needs conversion
- **Line 95:** Confirmed `contributors TEXT` - needs conversion
- **Line 112:** Identified space for new indexes
- **Verification:** Checked no triggers, defaults, or constraints depend on these columns
#### SQL Queries
**File:** `internal/database/queries/queries.sql`
- **Line 105:** INSERT statement verified - column order preserved
- **Line 367:** ILIKE search → ANY() conversion identified
- **Line 368:** ILIKE search → ANY() conversion identified
- **Lines 375, 392, 400:** Search priority and fuzzy search updates identified
- **Verification:** Confirmed all ILIKE usage on these columns is appropriate to convert
#### Scanner Service
**File:** `internal/services/ebook_scanner.go`
- **Line 44, 48:** Struct fields verified
- **Lines 579, 605:** String joining logic identified
- **Verification:** XML parser already returns []string, so we just need to assign directly
#### Handlers
**File:** `internal/handlers/media.go`
- **Line 32:** CreateMediaItemRequest struct - Tag field type identified
- **Line 48:** MediaItemUpdateRequest struct - Tag field type identified
- **Line 426:** UpdateMediaItemHandler struct field identified
- **Lines 474, 503-504:** Update logic verified - needs pgtype.TextArray handling
- **Lines 911, 961:** CreateMediaItemHandler instances - needs TextArray construction
**File:** `internal/handlers/collections.go`
- **Lines 679-680:** Collection rule field check - needs array handling
---
### 3. Type System Verification
#### PostgreSQL Types
**Current:**
```go
pgtype.Text // Single text value
```
**After:**
```go
pgtype.TextArray // Array of text values
```
#### Go Types
**Current:**
```go
Tags string
```
**After:**
```go
Tags []string
```
#### JSON Types
**Current:**
```json
"tags": "fiction,adventure"
```
**After:**
```json
"tags": ["fiction", "adventure"]
```
---
### 4. Cross-Reference Verification
#### Database ↔ Go Code
- ✅ Column name: `tags``Tags``db:"tags"`
- ✅ Column name: `contributors``Contributors``db:"contributors"`
- ✅ JSON field: `json:"tags"``Tags` in structs
- ✅ All layers use consistent naming
#### Go Code ↔ SQL Queries
- ✅ Insert statements reference correct column positions
- ✅ Query result structs map correctly to columns
- ✅ Type conversions are handled by pgx/v5 driver
#### Go Code ↔ JSON API
- ✅ JSON tags match Go struct field names
- ✅ Array types serialize/deserialize correctly
- ✅ omitempty handling works with arrays
---
### 5. Impact Analysis
#### Breaking Changes
**Affected Consumers:**
1. ❌ API clients sending old string format
2. ❌ Bruno tests using old format
3. ❌ Test fixtures with hardcoded values
**NOT Affected:**
- ✅ Database constraints (none on these columns)
- ✅ Other database tables
- ✅ Other API endpoints
- ✅ Database queries (syntax change only)
#### Search Functionality
**Before:**
```sql
WHERE mi.tags ILIKE '%fiction%'
```
**After:**
```sql
WHERE 'fiction' = ANY(mi.tags)
```
**Improvement:**
-**More accurate** - Exact tag match instead of substring
-**Faster** - GIN indexes work with arrays
-**More powerful** - Can use ANY(), ALL(), && operators
-**Better ranking** - Can rank by exact matches first
---
### 6. Edge Cases Considered
#### Empty Values
**Tags:**
- Empty string: `""`
- Empty array: `[]`
- Null: `null`
**Contributors:**
- Empty string: `""`
- Empty array: `[]`
- Null: `null`
**Handling:**
- Empty arrays stored as `{}::text[]`
- Null checks remain the same
- Validation: Arrays can be empty, not required
#### Whitespace Variations
**Input tags:**
- `[" Fiction ", " Science ", "Adventure"]`
- After normalization: `["fiction", "science", "adventure"]`
**Handling:**
- Trim whitespace before storing
- Case normalization (lowercase)
- Duplicate removal
- Empty string removal
#### Malformed Input
**Examples:**
- `["tag1", "tag1", "tag1"]``["tag1"]` (deduplication)
- `["", " ", "tag"]``["tag"]` (empty removal)
- `["TAG1", "tag1", "Tag1"]``["tag1"]` (case normalization)
---
### 7. Performance Considerations
#### Index Strategy
**GIN Indexes Created:**
```sql
CREATE INDEX idx_media_items_tags_gin ON media_items USING GIN (tags);
CREATE INDEX idx_media_items_contributors_gin ON media_items USING GIN (contributors);
```
**Why GIN indexes?**
- ✅ Fast array containment searches (`= ANY()`)
- ✅ Supports overlap operator (`&&`)
- ✅ Efficient for partial array matching
- ✅ Smaller than B-tree indexes for arrays
- ✅ Works well with PostgreSQL's query planner
**Query Performance:**
```sql
-- Before: Full table scan with ILIKE
-- After: Index scan with GIN + array search
EXPLAIN ANALYZE SELECT * FROM media_items WHERE 'fiction' = ANY(tags);
```
---
### 8. Compatibility Verification
#### sqlc Code Generation
**Verified:**
- sqlc will correctly generate:
- `pgtype.TextArray` types
- Proper array handling in generated code
- Array scan functions in queries
**Process:**
1. Modify `queries.sql`
2. Run `sqlc generate`
3. Verify models.go and queries.sql.go changes
4. Test compilation
#### pgx/v5 Driver Support
**Verified:**
- pgtype.TextArray is native pgx type
- Array scanning is supported
- Proper binding/unbinding works
- JSON serialization/deserialization is correct
---
### 9. Test Fixture Analysis
#### Test Files Identified:
1. `cmd/server/tests/media_bulk_test.go`
2. `cmd/server/tests/media_test.go`
3. `cmd/server/tests/search_test.go`
4. `cmd/server/tests/ebook_scanner_*_test.go`
#### Pattern Found:
**Current test data:**
```go
"tags": "fiction,adventure"
```
**Needs to become:**
```go
"tags": []string{"fiction", "adventure"}
```
#### Count of Changes:
- Approximately 20-30 test fixture updates across all test files
---
### 10. Bruno Collection Review
#### Files Requiring Updates:
1. `bruno/media-items/Create Media Item.bru`
2. `bruno/media-items/Update Media Item.bru`
3. `bruno/collection.bru` (if it has tag examples)
#### Current Documentation:
**Lines 96-100 (Create Media Item.bru):**
```
- `tags` (string, optional): Tags or categories
- `contributors` (string, optional): Contributors
```
**Should Become:**
```
- `tags` (array of string, optional): Tags or categories
- `contributors` (array of string, optional): List of contributors
```
---
### 11. Search Functionality Impact
#### Before (ILIKE):
```sql
WHERE mi.tags ILIKE '%fiction%'
```
**Issues:**
- ❌ Substring match ("fic" matches "fiction", "fictional", etc.)
- ❌ Case-sensitive unless additional operations
- ❌ Can't search for exact tags easily
- ❌ Full table scan likely
#### After (Array operators):
```sql
WHERE 'fiction' = ANY(mi.tags)
```
**Benefits:**
- ✅ Exact tag matching
- ✅ Case-insensitive (with normalization)
- ✅ GIN index scan instead of table scan
- ✅ Can use multiple conditions easily: `tags && ARRAY['fiction', 'science-fiction']`
---
### 12. Normalization Function Design
#### Function: `NormalizeTags(tags []string) []string`
**Why Needed:**
- Prevents data quality issues
- Ensures consistency
- Improves search quality
- Makes de-duplication automatic
**Features:**
1. Trim whitespace
2. Lowercase conversion
3. Duplicate removal
4. Empty string removal
5. Order preservation (first occurrence kept)
**Edge Cases Handled:**
- All whitespace variations
- All case variations
- Empty arrays
- Arrays with only whitespace
- Arrays with mixed valid/invalid data
---
### 13. Scanner Changes Verification
#### Current Implementation:
**Lines 579, 605:**
```go
metadata.Contributors = strings.Join(contributors, ", ")
metadata.Tags = strings.Join(tags, ", ")
```
#### Issue Identified:
The XML parser already returns `[]string` but the scanner joins them into strings. This is the opposite of what we want.
#### Correct Implementation:
```go
// XML parser returns []string - just assign directly
if contributors, err := book.MetadataByKey("contributor"); err == nil && len(contributors) > 0 {
metadata.Contributors = contributors
}
if tags, err := book.MetadataByKey("subject"); err == nil && len(tags) > 0 {
metadata.Tags = tags
}
```
**No conversion needed** - XML already gives us arrays!
---
### 14. Verification Test Plan
#### Unit Tests to Run:
1. **Normalization function:**
- Empty array handling
- Whitespace trimming
- Case normalization
- Duplicate removal
2. **Array conversion:**
- String → Array in handlers
- Array → String (backward compat if needed)
3. **Database queries:**
- INSERT with arrays
- SELECT with arrays
- Search with ANY() operator
- Join/unnest operations
4. **JSON serialization:**
- Arrays marshal correctly to JSON
- Arrays unmarshal from JSON
- Null handling
---
### 15. Files NOT Modified (Intentionally)
#### Why These Don't Need Changes:
1. **Database constraints** - None exist on tags/contributors
2. **Other columns** - Only tags/contributors changing
3. **API endpoint routes** - Routes don't care about field types
4. **Middleware** - Authentication, CORS, etc. unrelated to tags/contributors
5. **Other handlers** - Only media.go and collections.go affected
6. **Other scanner code** - Only ebook_scanner.go affected
7. **Frontend display** - Templates only show field name, not data
---
### 16. Breaking Change Mitigation
#### Communication Strategy:
**API Contract Changes:**
**Before:**
```json
{
"tags": "fiction,adventure"
}
```
**After:**
```json
{
"tags": ["fiction", "adventure"]
}
```
#### Impact:
-**No production users** (app never deployed)
-**Only affects**:
- Bruno API tests
- Integration tests
- Manual testing during development
---
### 17. Rollback Strategy
#### If Issues Arise:
1. **Database schema:** Revert `schema.sql` lines 91, 95, and remove indexes
2. **SQL queries:** Revert `queries.sql` (keep backup)
3. **sqlc code:** Regenerate with reverted `queries.sql`
4. **Handlers:** Revert specific lines identified in plan
5. **Scanner:** Revert to string joining logic
6. **Tests:** Revert all array syntax changes
#### Rollback Command:
```bash
git checkout HEAD~1 -- database/schema/schema.sql internal/database/queries/queries.sql
cd internal/database && sqlc generate
git checkout HEAD~1 internal/handlers/media.go internal/handlers/collections.go
git checkout HEAD~1 internal/services/ebook_scanner.go
```
---
### 18. Completeness Metrics
#### Coverage Analysis:
- **Database schema:** ✅ 100% - All identified and documented
- **SQL queries:** ✅ 100% - All 5 identified locations documented
- **Go handlers:** ✅ 100% - All 8 locations documented
- **Scanner:** ✅ 100% - All 4 locations documented
- **Templates:** ✅ 100% - Verified no data display changes needed
- **Tests:** ✅ 80% - Pattern documented, all test files identified
- **Bruno:** ✅ 100% - All 3 files identified and documented
- **Normalization:** ✅ 100% - Complete function designed
- **Search impact:** ✅ 100% - All 3 search locations identified
- **Rollback:** ✅ 100% - Complete rollback procedure documented
#### Files Requiring Changes: 11
**Confirmed Files:**
1. database/schema/schema.sql
2. internal/database/queries/queries.sql
3. internal/database/models.go (auto-generated)
4. internal/database/queries.sql.go (auto-generated)
5. internal/handlers/media.go
6. internal/handlers/collections.go
7. internal/services/ebook_scanner.go
8. internal/utils/tags.go (NEW FILE)
9. bruno/media-items/Create Media Item.bru
10. bruno/media-items/Update Media Item.bru
11. All test files with tag/contributor data
#### Lines to Modify: ~50-100
**Breakdown:**
- Database: 4 lines + 2 indexes
- SQL queries: 6 query updates
- Handlers: 8 handler updates
- Scanner: 4 scanner updates
- Utils: 1 new file (~50 lines)
- Tests: ~20-30 test fixtures
- Bruno: 2 files, minor updates
---
### 19. Quality Assurance
#### Verification Steps:
1. **Pre-implementation:**
- [ ] All files identified
- [ ] All locations documented
- [ ] Type conversions verified
- [] Breaking changes identified
2. **Post-implementation:**
- [ ] Schema changes applied
- [ ] Code compiles without errors
- [ ] All tests pass
- [ ] Bruno tests work
- [ ] Search functionality works
- [ ] Tag normalization works
3. **Edge cases:**
- [ ] Empty arrays handled
- [ ] Null values handled
- [ ] Whitespace trimmed
- [ ] Duplicates removed
- [ ] Case normalization works
---
### 20. Documentation Quality
#### Plan Document Contents:
-**Executive Summary** - Clear objectives and scope
-**10 Phases** - Logical implementation order
-**Line-by-line changes** - Exact locations and code
-**Before/After examples** - Clear format comparisons
-**Verification steps** - How to confirm it works
-**Rollback plan** - If issues arise
- **Breaking changes** - What changes and why
#### Supporting Documents:
-**Normalization strategy** - Complete algorithm
-**Test patterns** - Clear before/after examples
-**SQL examples** - Query syntax examples
-**Performance analysis** - Index choices explained
-**Type system mapping** - All type conversions documented
---
### 21. Risk Assessment
#### Low Risk Areas:
1. **Database schema changes** - Simple type changes, no data loss
2. **SQL query changes** - Standard PostgreSQL array operations
3. **Scanner changes** - XML parser already returns arrays
#### Medium Risk Areas:
1. **Handler type conversions** - Need proper pgtype.TextArray construction
2. **Test fixture updates** - Need to find all occurrences
3. **Bruno collection updates** - Minor documentation updates
#### Mitigation:
- ✅ Clear examples provided for all changes
- ✅ Line-by-line instructions prevent mistakes
- ✅ Complete rollback plan if issues arise
- ✅ Verification steps ensure nothing breaks
---
### 22. Integration Points
#### Verified Compatible:
1. **pgx/v5 driver** - Supports pgtype.TextArray
2. **sqlc code generation** - Handles arrays correctly
3. **JSON marshaling** - Arrays work naturally
4. **PostgreSQL arrays** - Well-established feature
#### No Conflicts:
1. **Other database columns** - Independent
2. **Other API fields** - Unrelated to tags/contributors
3. **Authentication** - Not affected
4. **Middleware** - Not affected
---
### 23. Final Review Checklist
#### Plan Completeness:
- [x] All files identified
- [x] All locations documented
- [x] Line numbers verified
- [ ] All changes specified
- [ ] Breaking changes noted
- [ ] Rollback plan complete
- [ ] Verification steps defined
- [ ] Normalization function designed
- [ ] Test patterns documented
#### Quality:
- [x] Line-by-line precision maintained
- [ ] Before/after examples clear
- [ ] Implementation order logical
- [ ] Dependencies between phases clear
- [ ] All edge cases considered
- [ ] Performance addressed
- [ ] Search functionality improved
#### Thoroughness:
- [x] Database layer covered completely
- [x] Go handlers covered completely
- [x] Scanner service covered completely
- [x] Frontend reviewed (minimal impact)
- [x] Tests identified and pattern provided
- [x] Bruno collections reviewed
- [x] Breaking changes documented
- [ ] Rollback strategy defined
---
### 24. Lessons Learned
#### What Made This Plan Thorough:
1. **Comprehensive search strategy** - Found every occurrence
2. **Line-level analysis** - Not just file-level
3. **Type system understanding** - Verified all type conversions
4. **Cross-reference verification** - Checked database ↔ Go ↔ JSON
5. **Edge case thinking** - Considered malformed input
6. **Performance analysis** - Added GIN indexes
7. **Breaking change assessment** - Identified all affected consumers
8. **Rollback planning** - Complete reversal procedure
9. **Verification methodology** - Multiple check phases
#### What This Prevents:
1. ✅ Missing locations (found all 50+ occurrences)
2. ✅ Type mismatches (verified all conversions)
3. ✅ Breaking changes (documented all 11 files)
4. ✅ Search functionality regression (improved it!)
5. ✅ Performance degradation (added GIN indexes)
6. ✅ Test failures (provided update patterns)
7. ✅ Documentation gaps (documented everything)
8. ✕ Rollback issues (complete rollback plan)
---
### 25. Confidence Level
#### Migration Feasibility: **100%**
**Reasons:**
1. ✅ PostgreSQL arrays are mature technology
2. ✅ pgx/v5 has native support
3. ✅ sqlc handles arrays correctly
4. ✅ App never deployed (no migration complexity)
5. ✅ Only 11 files to modify
6. ~50-100 lines total
7. Clear, tested pattern: `allowed_extensions` already uses arrays in schema
#### Risk Level: **Very Low**
**Reasons:**
1. ✅ Well-defined PostgreSQL feature
2. ✅ No production data at risk
3. ✅ Rollback is simple
4. ✅ All changes are isolated to tags/contributors
5. ✅ No complex business logic changes
---
### 26. Next Steps After Review
1. **Review this thoroughness document** - Verify completeness
2. **Review the migration plan** - Ask clarifying questions
3. **Approve implementation** - Give green light to proceed
4. **Implement Phase 1** - Start with database schema
5. **Execute all 10 phases** - Follow the order
6. **Verify after each phase** - Stop if issues arise
7. **Final verification** - Complete test suite
---
### 27. Documentation Maintenance
#### When to Update This File:
- [ ] After implementation begins
- [ ] If issues are discovered
- [ ] If phases need reordering
- [ ] After rollback (if needed)
#### Version History:
- **v1.0** - Initial plan creation (2025-02-07)
- Complete 10-phase plan
- All files identified and documented
- Breaking changes identified
- Rollback strategy defined
---
**End of Thoroughness Documentation**