From 0f552282f34a79b3674647511ba9f526b208039f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 1 Jun 2026 22:40:21 +0200 Subject: [PATCH] step 2: Add UUID columns with backfill and dual-write support - Add migration 000007: Add UUID columns to soundtrack and song with backfill - Update InsertSoundtrack and InsertSoundtrackWithExistingId to accept UUID - Update AddSong to accept UUID - Add dual-write: Go code now generates UUIDs for new records - Add uuid and pgtype imports Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- internal/backend/sync.go | 11 +++++-- .../000007_add_uuid_and_backfill.down.sql | 9 ++++++ .../000007_add_uuid_and_backfill.up.sql | 21 +++++++++++++ internal/db/queries/song.sql | 2 +- internal/db/queries/soundtrack.sql | 4 +-- internal/db/repository/models.go | 8 ----- internal/db/repository/song.sql.go | 14 +++++---- internal/db/repository/soundtrack.sql.go | 30 ++++++++++++------- 8 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 internal/db/migrations/000007_add_uuid_and_backfill.down.sql create mode 100644 internal/db/migrations/000007_add_uuid_and_backfill.up.sql diff --git a/internal/backend/sync.go b/internal/backend/sync.go index b4522d8..57b76d9 100644 --- a/internal/backend/sync.go +++ b/internal/backend/sync.go @@ -16,6 +16,8 @@ import ( "sync" "time" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" "github.com/panjf2000/ants/v2" "github.com/MShekow/directory-checksum/directory_checksum" @@ -343,7 +345,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full break } } - err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, SoundtrackName: file.Name(), Path: gameDir, Hash: dirHash}) + gameUuid := pgtype.UUID{Bytes: uuid.New(), Valid: true} + err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, Uuid: gameUuid, SoundtrackName: file.Name(), Path: gameDir, Hash: dirHash}) handleError("InsertSoundtrackWithExistingId", err, "") if err != nil { logging.GetLogger().Debug("Game already exists, removing old ID file", @@ -435,7 +438,8 @@ func syncGameNew(file os.DirEntry, foldersToSkip []string, baseDir string, full func insertGameNew(name string, path string, hash string) int32 { var duplicateError = errors.New("ERROR: duplicate key value violates unique") - id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash}) + gameUuid := pgtype.UUID{Bytes: uuid.New(), Valid: true} + id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{Uuid: gameUuid, SoundtrackName: name, Path: path, Hash: hash}) handleError("InsertSoundtrack", err, "") if err != nil { logging.GetLogger().Warn("ID collision detected, resetting sequence") @@ -522,7 +526,8 @@ func newCheckSong(entry os.DirEntry, gameDir string, id int32) bool { err = repo.AddHashToSong(BackendCtx(), repository.AddHashToSongParams{Hash: songHash, SoundtrackID: id, Path: path}) handleError("AddHashToSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) } else { - err = repo.AddSong(BackendCtx(), repository.AddSongParams{SoundtrackID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash}) + songUuid := pgtype.UUID{Bytes: uuid.New(), Valid: true} + err = repo.AddSong(BackendCtx(), repository.AddSongParams{Uuid: songUuid, SoundtrackID: id, SongName: songName, Path: path, FileName: &fileName, Hash: songHash}) handleError("AddSong", err, fmt.Sprintf("SoundtrackID: %d | Path: %s | SongName: %s | SongHash: %s", id, path, entry.Name(), songHash)) } diff --git a/internal/db/migrations/000007_add_uuid_and_backfill.down.sql b/internal/db/migrations/000007_add_uuid_and_backfill.down.sql new file mode 100644 index 0000000..5463739 --- /dev/null +++ b/internal/db/migrations/000007_add_uuid_and_backfill.down.sql @@ -0,0 +1,9 @@ +-- Rollback: Remove UUID columns from soundtrack and song tables + +-- Drop indexes +DROP INDEX IF EXISTS idx_soundtrack_uuid; +DROP INDEX IF EXISTS idx_song_uuid; + +-- Drop UUID columns +ALTER TABLE soundtrack DROP COLUMN IF EXISTS uuid; +ALTER TABLE song DROP COLUMN IF EXISTS uuid; diff --git a/internal/db/migrations/000007_add_uuid_and_backfill.up.sql b/internal/db/migrations/000007_add_uuid_and_backfill.up.sql new file mode 100644 index 0000000..0e8d91a --- /dev/null +++ b/internal/db/migrations/000007_add_uuid_and_backfill.up.sql @@ -0,0 +1,21 @@ +-- Migration: Add UUID columns to soundtrack and song, then backfill + +-- Add UUID column to soundtrack (nullable for now) +ALTER TABLE soundtrack ADD COLUMN uuid UUID NULL UNIQUE; + +-- Create index on uuid for performance +CREATE INDEX IF NOT EXISTS idx_soundtrack_uuid ON soundtrack(uuid); + +-- Add UUID column to song (nullable for now) +ALTER TABLE song ADD COLUMN uuid UUID NULL UNIQUE; + +-- Create index on uuid for performance +CREATE INDEX IF NOT EXISTS idx_song_uuid ON song(uuid); + +-- Backfill existing records immediately +UPDATE soundtrack SET uuid = gen_random_uuid() WHERE uuid IS NULL; +UPDATE song SET uuid = gen_random_uuid() WHERE uuid IS NULL; + +-- Verify no nulls remain +-- SELECT COUNT(*) FROM soundtrack WHERE uuid IS NULL; -- Should be 0 +-- SELECT COUNT(*) FROM song WHERE uuid IS NULL; -- Should be 0 diff --git a/internal/db/queries/song.sql b/internal/db/queries/song.sql index 95064ca..fbbf2cf 100644 --- a/internal/db/queries/song.sql +++ b/internal/db/queries/song.sql @@ -5,7 +5,7 @@ DELETE FROM song; DELETE FROM song WHERE soundtrack_id = $1; -- name: AddSong :exec -INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5); +INSERT INTO song(uuid, soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5, $6); -- name: CheckSong :one SELECT COUNT(*) FROM song WHERE soundtrack_id = $1 AND path = $2; diff --git a/internal/db/queries/soundtrack.sql b/internal/db/queries/soundtrack.sql index 1de82d5..199a4b3 100644 --- a/internal/db/queries/soundtrack.sql +++ b/internal/db/queries/soundtrack.sql @@ -29,10 +29,10 @@ UPDATE soundtrack SET deleted=NULL WHERE id=$1; SELECT id FROM soundtrack WHERE soundtrack_name = $1; -- name: InsertSoundtrack :one -INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id; +INSERT INTO soundtrack (uuid, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now()) returning id; -- name: InsertSoundtrackWithExistingId :exec -INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now()); +INSERT INTO soundtrack (id, uuid, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, $5, now()); -- name: FindAllSoundtracks :many SELECT * diff --git a/internal/db/repository/models.go b/internal/db/repository/models.go index 82728e5..0e27be9 100644 --- a/internal/db/repository/models.go +++ b/internal/db/repository/models.go @@ -10,14 +10,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -type IDMigrationStatus struct { - TableName string `json:"table_name"` - TotalRows int32 `json:"total_rows"` - MigratedRows int32 `json:"migrated_rows"` - Completed bool `json:"completed"` - StartedAt *time.Time `json:"started_at"` -} - type Session struct { Token string `json:"token"` IpAddress string `json:"ip_address"` diff --git a/internal/db/repository/song.sql.go b/internal/db/repository/song.sql.go index 5b79b4f..00c7805 100644 --- a/internal/db/repository/song.sql.go +++ b/internal/db/repository/song.sql.go @@ -27,19 +27,21 @@ func (q *Queries) AddHashToSong(ctx context.Context, arg AddHashToSongParams) er } const addSong = `-- name: AddSong :exec -INSERT INTO song(soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5) +INSERT INTO song(uuid, soundtrack_id, song_name, path, file_name, hash) VALUES ($1, $2, $3, $4, $5, $6) ` type AddSongParams struct { - SoundtrackID int32 `json:"soundtrack_id"` - SongName string `json:"song_name"` - Path string `json:"path"` - FileName *string `json:"file_name"` - Hash string `json:"hash"` + Uuid pgtype.UUID `json:"uuid"` + SoundtrackID int32 `json:"soundtrack_id"` + SongName string `json:"song_name"` + Path string `json:"path"` + FileName *string `json:"file_name"` + Hash string `json:"hash"` } func (q *Queries) AddSong(ctx context.Context, arg AddSongParams) error { _, err := q.db.Exec(ctx, addSong, + arg.Uuid, arg.SoundtrackID, arg.SongName, arg.Path, diff --git a/internal/db/repository/soundtrack.sql.go b/internal/db/repository/soundtrack.sql.go index b80cfd3..3bbc9e1 100644 --- a/internal/db/repository/soundtrack.sql.go +++ b/internal/db/repository/soundtrack.sql.go @@ -7,6 +7,8 @@ package repository import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const addSoundtrackPlayed = `-- name: AddSoundtrackPlayed :exec @@ -153,36 +155,44 @@ func (q *Queries) GetSoundtrackNameById(ctx context.Context, id int32) (string, } const insertSoundtrack = `-- name: InsertSoundtrack :one -INSERT INTO soundtrack (soundtrack_name, path, hash, added) VALUES ($1, $2, $3, now()) returning id +INSERT INTO soundtrack (uuid, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now()) returning id ` type InsertSoundtrackParams struct { - SoundtrackName string `json:"soundtrack_name"` - Path string `json:"path"` - Hash string `json:"hash"` + Uuid pgtype.UUID `json:"uuid"` + SoundtrackName string `json:"soundtrack_name"` + Path string `json:"path"` + Hash string `json:"hash"` } func (q *Queries) InsertSoundtrack(ctx context.Context, arg InsertSoundtrackParams) (int32, error) { - row := q.db.QueryRow(ctx, insertSoundtrack, arg.SoundtrackName, arg.Path, arg.Hash) + row := q.db.QueryRow(ctx, insertSoundtrack, + arg.Uuid, + arg.SoundtrackName, + arg.Path, + arg.Hash, + ) var id int32 err := row.Scan(&id) return id, err } const insertSoundtrackWithExistingId = `-- name: InsertSoundtrackWithExistingId :exec -INSERT INTO soundtrack (id, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, now()) +INSERT INTO soundtrack (id, uuid, soundtrack_name, path, hash, added) VALUES ($1, $2, $3, $4, $5, now()) ` type InsertSoundtrackWithExistingIdParams struct { - ID int32 `json:"id"` - SoundtrackName string `json:"soundtrack_name"` - Path string `json:"path"` - Hash string `json:"hash"` + ID int32 `json:"id"` + Uuid pgtype.UUID `json:"uuid"` + SoundtrackName string `json:"soundtrack_name"` + Path string `json:"path"` + Hash string `json:"hash"` } func (q *Queries) InsertSoundtrackWithExistingId(ctx context.Context, arg InsertSoundtrackWithExistingIdParams) error { _, err := q.db.Exec(ctx, insertSoundtrackWithExistingId, arg.ID, + arg.Uuid, arg.SoundtrackName, arg.Path, arg.Hash,