From 1d735c3a5f5a78996d6ddbe5c787adf040fe2f21 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Fri, 13 Mar 2026 13:38:39 -0400 Subject: [PATCH] fix(12-02): reorder orphan cleanup to delete FK children before recordings recording_genres and release_group_recordings reference recordings.id, so they must be deleted BEFORE the recordings table is cleaned. Also extends toast duration to 8s for readability. --- backend/library/crud.go | 41 +++++----- .../src/components/config-page/config-page.ts | 2 +- frontend/wailsjs/go/library/Library.d.ts | 19 +++-- frontend/wailsjs/go/library/Library.js | 24 +++--- frontend/wailsjs/go/models.ts | 82 +++++++++++++++---- frontend/wailsjs/go/queue/Queue.d.ts | 2 + frontend/wailsjs/go/queue/Queue.js | 4 + 7 files changed, 119 insertions(+), 55 deletions(-) diff --git a/backend/library/crud.go b/backend/library/crud.go index 523d4cd..546de57 100644 --- a/backend/library/crud.go +++ b/backend/library/crud.go @@ -288,7 +288,28 @@ func (l *Library) RemoveLibrary(id int64) (*RemovalSummary, error) { tracksDeleted, _ := result.RowsAffected() - // 7. Delete orphaned recordings. + // 7. Delete orphaned recording_genres (must run BEFORE recordings + // because recording_genres.recording_id references recordings.id). + // SAFETY: Hand-crafted orphan cleanup SQL. Parameterless. + if _, err := tx.ExecContext(l.ctx, + `DELETE FROM recording_genres WHERE recording_id NOT IN ( + SELECT DISTINCT recording_id FROM audio_files + )`); err != nil { + return nil, fmt.Errorf("could not delete orphaned recording_genres: %w", err) + } + + // 8. Delete orphaned release_group_recordings (must run BEFORE + // recordings because release_group_recordings.recording_id + // references recordings.id). + // SAFETY: Hand-crafted orphan cleanup SQL. Parameterless. + if _, err := tx.ExecContext(l.ctx, + `DELETE FROM release_group_recordings WHERE recording_id NOT IN ( + SELECT DISTINCT recording_id FROM audio_files + )`); err != nil { + return nil, fmt.Errorf("could not delete orphaned release_group_recordings: %w", err) + } + + // 9. Delete orphaned recordings (safe now that child tables are cleaned). // SAFETY: Hand-crafted orphan cleanup SQL. Reference-counting delete // with NOT IN subquery unsupported by sqlc. No user input. if _, err := tx.ExecContext(l.ctx, @@ -298,24 +319,6 @@ func (l *Library) RemoveLibrary(id int64) (*RemovalSummary, error) { return nil, fmt.Errorf("could not delete orphaned recordings: %w", err) } - // 8. Delete orphaned recording_genres. - // SAFETY: Hand-crafted orphan cleanup SQL. Parameterless. - if _, err := tx.ExecContext(l.ctx, - `DELETE FROM recording_genres WHERE recording_id NOT IN ( - SELECT id FROM recordings - )`); err != nil { - return nil, fmt.Errorf("could not delete orphaned recording_genres: %w", err) - } - - // 9. Delete orphaned release_group_recordings. - // SAFETY: Hand-crafted orphan cleanup SQL. Parameterless. - if _, err := tx.ExecContext(l.ctx, - `DELETE FROM release_group_recordings WHERE recording_id NOT IN ( - SELECT id FROM recordings - )`); err != nil { - return nil, fmt.Errorf("could not delete orphaned release_group_recordings: %w", err) - } - // 10. Delete orphaned release_groups. // SAFETY: Hand-crafted orphan cleanup SQL. Parameterless. result, err = tx.ExecContext(l.ctx, diff --git a/frontend/src/components/config-page/config-page.ts b/frontend/src/components/config-page/config-page.ts index 6157c31..987531c 100644 --- a/frontend/src/components/config-page/config-page.ts +++ b/frontend/src/components/config-page/config-page.ts @@ -1456,7 +1456,7 @@ export class ConfigPage extends LitElement { this.toastTimer = setTimeout(() => { this.toastVisible = false; - }, 4000); + }, 8000); } private handleConcurrencyChange = ( diff --git a/frontend/wailsjs/go/library/Library.d.ts b/frontend/wailsjs/go/library/Library.d.ts index f546b8e..44b7ea3 100755 --- a/frontend/wailsjs/go/library/Library.d.ts +++ b/frontend/wailsjs/go/library/Library.d.ts @@ -1,9 +1,10 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +import {sqlcgen} from '../models'; import {library} from '../models'; import {context} from '../models'; -export function AddLibrary(arg1:string):Promise; +export function AddLibrary(arg1:string):Promise; export function CancelAllScans():Promise; @@ -15,8 +16,6 @@ export function FullRescan():Promise; export function GetAlbumTracks(arg1:number):Promise>; -export function GetAllLibrariesWithTrackCounts():Promise>; - export function GetAlbumsByArtist(arg1:number):Promise>; export function GetAllAlbums():Promise>; @@ -25,6 +24,8 @@ export function GetAllArtists():Promise>; export function GetAllGenresWithCounts():Promise>; +export function GetAllLibrariesWithTrackCounts():Promise>; + export function GetAllTracks():Promise>; export function GetRemovalImpact(arg1:number):Promise; @@ -39,15 +40,13 @@ export function IsScanPaused():Promise; export function PauseScan():Promise; -export function RemoveLibrary(arg1:number):Promise; - -export function RenameLibrary(arg1:number, arg2:string):Promise; - export function QueuedLibraryNames():Promise>; -export function ResumeScan():Promise; +export function RemoveLibrary(arg1:number):Promise; -export function Scan():Promise; +export function RenameLibrary(arg1:number,arg2:string):Promise; + +export function ResumeScan():Promise; export function ScanAllLibraries():Promise; @@ -57,4 +56,6 @@ export function SearchTracks(arg1:string):Promise>; export function SetContext(arg1:context.Context):Promise; +export function SetRemovalHooks(arg1:library.RemovalHooks):Promise; + export function SetRescanHooks(arg1:library.RescanHooks):Promise; diff --git a/frontend/wailsjs/go/library/Library.js b/frontend/wailsjs/go/library/Library.js index 8b2e819..4678d3e 100755 --- a/frontend/wailsjs/go/library/Library.js +++ b/frontend/wailsjs/go/library/Library.js @@ -26,10 +26,6 @@ export function GetAlbumTracks(arg1) { return window['go']['library']['Library']['GetAlbumTracks'](arg1); } -export function GetAllLibrariesWithTrackCounts() { - return window['go']['library']['Library']['GetAllLibrariesWithTrackCounts'](); -} - export function GetAlbumsByArtist(arg1) { return window['go']['library']['Library']['GetAlbumsByArtist'](arg1); } @@ -46,6 +42,10 @@ export function GetAllGenresWithCounts() { return window['go']['library']['Library']['GetAllGenresWithCounts'](); } +export function GetAllLibrariesWithTrackCounts() { + return window['go']['library']['Library']['GetAllLibrariesWithTrackCounts'](); +} + export function GetAllTracks() { return window['go']['library']['Library']['GetAllTracks'](); } @@ -74,6 +74,10 @@ export function PauseScan() { return window['go']['library']['Library']['PauseScan'](); } +export function QueuedLibraryNames() { + return window['go']['library']['Library']['QueuedLibraryNames'](); +} + export function RemoveLibrary(arg1) { return window['go']['library']['Library']['RemoveLibrary'](arg1); } @@ -82,18 +86,10 @@ export function RenameLibrary(arg1, arg2) { return window['go']['library']['Library']['RenameLibrary'](arg1, arg2); } -export function QueuedLibraryNames() { - return window['go']['library']['Library']['QueuedLibraryNames'](); -} - export function ResumeScan() { return window['go']['library']['Library']['ResumeScan'](); } -export function Scan() { - return window['go']['library']['Library']['Scan'](); -} - export function ScanAllLibraries() { return window['go']['library']['Library']['ScanAllLibraries'](); } @@ -110,6 +106,10 @@ export function SetContext(arg1) { return window['go']['library']['Library']['SetContext'](arg1); } +export function SetRemovalHooks(arg1) { + return window['go']['library']['Library']['SetRemovalHooks'](arg1); +} + export function SetRescanHooks(arg1) { return window['go']['library']['Library']['SetRescanHooks'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 94dbcdc..b361e51 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -40,6 +40,20 @@ export namespace library { this.Name = source["Name"]; } } + export class GenreWithCount { + Name: string; + TrackCount: number; + + static createFrom(source: any = {}) { + return new GenreWithCount(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Name = source["Name"]; + this.TrackCount = source["TrackCount"]; + } + } export class Info { id: number; name: string; @@ -58,6 +72,18 @@ export namespace library { this.trackCount = source["trackCount"]; } } + export class RemovalHooks { + + + static createFrom(source: any = {}) { + return new RemovalHooks(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + + } + } export class RemovalImpact { trackCount: number; playlistsAffected: number; @@ -96,20 +122,6 @@ export namespace library { this.queueItemsRemoved = source["queueItemsRemoved"]; } } - export class GenreWithCount { - Name: string; - TrackCount: number; - - static createFrom(source: any = {}) { - return new GenreWithCount(source); - } - - constructor(source: any = {}) { - if ('string' === typeof source) source = JSON.parse(source); - this.Name = source["Name"]; - this.TrackCount = source["TrackCount"]; - } - } export class RescanHooks { @@ -606,6 +618,48 @@ export namespace queue { } +export namespace sqlcgen { + + export class Library { + ID: number; + Name: string; + Path: string; + // Go type: time + CreatedAt: any; + + static createFrom(source: any = {}) { + return new Library(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.ID = source["ID"]; + this.Name = source["Name"]; + this.Path = source["Path"]; + this.CreatedAt = this.convertValues(source["CreatedAt"], null); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } + +} + export namespace tracklist { export class Column { diff --git a/frontend/wailsjs/go/queue/Queue.d.ts b/frontend/wailsjs/go/queue/Queue.d.ts index 0cc75f0..abec65d 100755 --- a/frontend/wailsjs/go/queue/Queue.d.ts +++ b/frontend/wailsjs/go/queue/Queue.d.ts @@ -9,6 +9,8 @@ export function AddTracks(arg1:Array):Promise; export function Clear():Promise; +export function CompactAfterLibraryRemoval():Promise; + export function CycleRepeat():Promise; export function EmitCurrentState():Promise; diff --git a/frontend/wailsjs/go/queue/Queue.js b/frontend/wailsjs/go/queue/Queue.js index b3fcbc5..cd4307e 100755 --- a/frontend/wailsjs/go/queue/Queue.js +++ b/frontend/wailsjs/go/queue/Queue.js @@ -14,6 +14,10 @@ export function Clear() { return window['go']['queue']['Queue']['Clear'](); } +export function CompactAfterLibraryRemoval() { + return window['go']['queue']['Queue']['CompactAfterLibraryRemoval'](); +} + export function CycleRepeat() { return window['go']['queue']['Queue']['CycleRepeat'](); }