This commit is contained in:
6
db.go
6
db.go
@@ -31,6 +31,12 @@ func InitDB(dbPath string) error {
|
|||||||
return fmt.Errorf("failed to open database: %v", err)
|
return fmt.Errorf("failed to open database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable foreign key support in SQLite
|
||||||
|
_, err = DB.Exec("PRAGMA foreign_keys = ON")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to enable foreign keys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err = DB.Ping(); err != nil {
|
if err = DB.Ping(); err != nil {
|
||||||
return fmt.Errorf("failed to ping database: %v", err)
|
return fmt.Errorf("failed to ping database: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,41 @@ func (r *LibraryRepository) UpdatePath(id int, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LibraryRepository) Delete(id int) error {
|
func (r *LibraryRepository) Delete(id int) error {
|
||||||
_, err := r.db.Exec("DELETE FROM libraries WHERE id = ?", id)
|
tx, err := r.db.Begin()
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Delete the library — CASCADE removes media_files and songs automatically
|
||||||
|
_, err = tx.Exec("DELETE FROM libraries WHERE id = ?", id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up orphan albums (no songs reference them)
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
DELETE FROM albums WHERE id NOT IN (
|
||||||
|
SELECT DISTINCT album_id FROM songs WHERE album_id IS NOT NULL
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up orphan artists (no songs and no albums reference them)
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
DELETE FROM artists WHERE id NOT IN (
|
||||||
|
SELECT DISTINCT artist_id FROM songs WHERE artist_id IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT DISTINCT artist_id FROM albums WHERE artist_id IS NOT NULL
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LibraryRepository) GetSongsByLibraryWithDetails(libraryID int) ([]SongDetail, error) {
|
func (r *LibraryRepository) GetSongsByLibraryWithDetails(libraryID int) ([]SongDetail, error) {
|
||||||
|
|||||||
@@ -12,15 +12,11 @@ func NewMediaRepository(db *sql.DB) *MediaRepository {
|
|||||||
|
|
||||||
func (r *MediaRepository) Get(id int) (MediaFile, error) {
|
func (r *MediaRepository) Get(id int) (MediaFile, error) {
|
||||||
var m MediaFile
|
var m MediaFile
|
||||||
rows, err := r.db.Query("SELECT id, path, library_id FROM media_files WHERE id = ?", id)
|
err := r.db.QueryRow("SELECT id, path, library_id FROM media_files WHERE id = ?", id).Scan(
|
||||||
|
&m.ID, &m.Path, &m.LibraryID,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MediaFile{}, err
|
return MediaFile{}, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
if rows.Next() {
|
|
||||||
if err := rows.Scan(&m.ID, &m.Path, &m.LibraryID); err != nil {
|
|
||||||
return MediaFile{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,13 +55,11 @@ func (r *SongRepository) GetAllWithDetails() ([]SongDetail, error) {
|
|||||||
|
|
||||||
func (r *SongRepository) Get(id int) (Song, error) {
|
func (r *SongRepository) Get(id int) (Song, error) {
|
||||||
var song Song
|
var song Song
|
||||||
rows, err := r.db.Query("SELECT id, title, artist_id, album_id, duration, media_file_id FROM songs WHERE id = ?", id)
|
err := r.db.QueryRow("SELECT id, title, artist_id, album_id, duration, media_file_id FROM songs WHERE id = ?", id).Scan(
|
||||||
|
&song.ID, &song.Title, &song.ArtistID, &song.AlbumID, &song.Duration, &song.MediaFileID,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Song{}, err
|
return Song{}, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
|
||||||
if rows.Next() {
|
|
||||||
rows.Scan(&song.ID, &song.Title, &song.ArtistID, &song.AlbumID, &song.Duration, &song.MediaFileID)
|
|
||||||
}
|
|
||||||
return song, nil
|
return song, nil
|
||||||
}
|
}
|
||||||
|
|||||||
142
migrations/002_cascade_fk.go
Normal file
142
migrations/002_cascade_fk.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterMigration(
|
||||||
|
2,
|
||||||
|
"Add ON DELETE CASCADE to foreign keys",
|
||||||
|
migrateCascadeFKUp,
|
||||||
|
migrateCascadeFKDown,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateCascadeFKUp(tx *sql.Tx) error {
|
||||||
|
// Enable foreign keys for this transaction
|
||||||
|
_, err := tx.Exec("PRAGMA foreign_keys = OFF")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Rebuild media_files with FK + CASCADE
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
CREATE TABLE media_files_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
library_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (library_id) REFERENCES libraries(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`INSERT INTO media_files_new SELECT id, path, library_id FROM media_files`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("DROP TABLE media_files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("ALTER TABLE media_files_new RENAME TO media_files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Rebuild songs with FK + CASCADE on media_file_id
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
CREATE TABLE songs_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
album_id INTEGER,
|
||||||
|
artist_id INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
media_file_id INTEGER UNIQUE,
|
||||||
|
FOREIGN KEY (album_id) REFERENCES albums(id),
|
||||||
|
FOREIGN KEY (artist_id) REFERENCES artists(id),
|
||||||
|
FOREIGN KEY (media_file_id) REFERENCES media_files(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`INSERT INTO songs_new SELECT id, title, album_id, artist_id, duration, media_file_id FROM songs`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("DROP TABLE songs")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("ALTER TABLE songs_new RENAME TO songs")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateCascadeFKDown(tx *sql.Tx) error {
|
||||||
|
_, err := tx.Exec("PRAGMA foreign_keys = OFF")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert songs
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
CREATE TABLE songs_old (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
album_id INTEGER,
|
||||||
|
artist_id INTEGER,
|
||||||
|
duration INTEGER,
|
||||||
|
media_file_id INTEGER UNIQUE,
|
||||||
|
FOREIGN KEY (album_id) REFERENCES albums(id),
|
||||||
|
FOREIGN KEY (artist_id) REFERENCES artists(id),
|
||||||
|
FOREIGN KEY (media_file_id) REFERENCES media_files(id)
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`INSERT INTO songs_old SELECT id, title, album_id, artist_id, duration, media_file_id FROM songs`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("DROP TABLE songs")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("ALTER TABLE songs_old RENAME TO songs")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert media_files
|
||||||
|
_, err = tx.Exec(`
|
||||||
|
CREATE TABLE media_files_old (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
library_id INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(`INSERT INTO media_files_old SELECT id, path, library_id FROM media_files`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("DROP TABLE media_files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec("ALTER TABLE media_files_old RENAME TO media_files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user