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.
This commit is contained in:
2026-03-13 13:38:39 -04:00
parent cf004986c9
commit 1d735c3a5f
7 changed files with 119 additions and 55 deletions
+22 -19
View File
@@ -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,
@@ -1456,7 +1456,7 @@ export class ConfigPage extends LitElement {
this.toastTimer = setTimeout(() => {
this.toastVisible = false;
}, 4000);
}, 8000);
}
private handleConcurrencyChange = (
+10 -9
View File
@@ -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<library.Info>;
export function AddLibrary(arg1:string):Promise<sqlcgen.Library>;
export function CancelAllScans():Promise<void>;
@@ -15,8 +16,6 @@ export function FullRescan():Promise<library.ScanMetrics>;
export function GetAlbumTracks(arg1:number):Promise<Array<library.Track>>;
export function GetAllLibrariesWithTrackCounts():Promise<Array<library.Info>>;
export function GetAlbumsByArtist(arg1:number):Promise<Array<library.Album>>;
export function GetAllAlbums():Promise<Array<library.Album>>;
@@ -25,6 +24,8 @@ export function GetAllArtists():Promise<Array<library.Artist>>;
export function GetAllGenresWithCounts():Promise<Array<library.GenreWithCount>>;
export function GetAllLibrariesWithTrackCounts():Promise<Array<library.Info>>;
export function GetAllTracks():Promise<Array<library.Track>>;
export function GetRemovalImpact(arg1:number):Promise<library.RemovalImpact>;
@@ -39,15 +40,13 @@ export function IsScanPaused():Promise<boolean>;
export function PauseScan():Promise<void>;
export function RemoveLibrary(arg1:number):Promise<library.RemovalSummary>;
export function RenameLibrary(arg1:number, arg2:string):Promise<void>;
export function QueuedLibraryNames():Promise<Array<string>>;
export function ResumeScan():Promise<void>;
export function RemoveLibrary(arg1:number):Promise<library.RemovalSummary>;
export function Scan():Promise<library.ScanMetrics>;
export function RenameLibrary(arg1:number,arg2:string):Promise<void>;
export function ResumeScan():Promise<void>;
export function ScanAllLibraries():Promise<void>;
@@ -57,4 +56,6 @@ export function SearchTracks(arg1:string):Promise<Array<library.Track>>;
export function SetContext(arg1:context.Context):Promise<void>;
export function SetRemovalHooks(arg1:library.RemovalHooks):Promise<void>;
export function SetRescanHooks(arg1:library.RescanHooks):Promise<void>;
+12 -12
View File
@@ -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);
}
+68 -14
View File
@@ -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 {
+2
View File
@@ -9,6 +9,8 @@ export function AddTracks(arg1:Array<string>):Promise<void>;
export function Clear():Promise<void>;
export function CompactAfterLibraryRemoval():Promise<void>;
export function CycleRepeat():Promise<void>;
export function EmitCurrentState():Promise<void>;
+4
View File
@@ -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']();
}