From 9c7e5a96344a81bf132de487b4763f1dc3ff6df9 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Wed, 4 Mar 2026 19:21:58 -0500 Subject: [PATCH] feat(06-01): create track_metadata VIEW schema and migration 4 - Add track_metadata_view.sql for sqlc VIEW awareness - Add migration4TrackMetadataView for existing databases - sqlc generate produces TrackMetadatum model from VIEW - migration2 inline JOIN preserved (runs before VIEW exists) --- backend/database/database.go | 79 +++++++++++++++++++ .../sql/schemas/track_metadata_view.sql | 36 +++++++++ backend/database/sql/sqlcgen/models.go | 20 +++++ 3 files changed, 135 insertions(+) create mode 100644 backend/database/sql/schemas/track_metadata_view.sql diff --git a/backend/database/database.go b/backend/database/database.go index 088e1a2..05009d4 100644 --- a/backend/database/database.go +++ b/backend/database/database.go @@ -276,6 +276,15 @@ func runMigrations( logger.Info("migration 3 complete") } + // Migration 4: create track_metadata VIEW. + if version < 4 { + if err := migration4TrackMetadataView( + ctx, db, logger, + ); err != nil { + return err + } + } + return nil } @@ -379,6 +388,76 @@ func migration2BasenameAndFTS( return nil } +// migration4TrackMetadataView creates the track_metadata VIEW that +// consolidates the 5-table JOIN used by FTS5 search queries. +// Fresh databases get the VIEW from the embedded schema file; +// this migration covers databases created before the VIEW existed. +func migration4TrackMetadataView( + ctx context.Context, + db *sql.DB, + logger *slog.Logger, +) error { + logger.Info( + "applying migration 4: track_metadata VIEW", + ) + + if _, err := db.ExecContext(ctx, ` + CREATE VIEW IF NOT EXISTS track_metadata AS + SELECT + af.id, + af.file_path, + af.length_milliseconds, + COALESCE(r.name, '') AS title, + COALESCE(ac.text, '') AS artist_name, + r.track_number, + r.disc_number, + COALESCE(rg.name, '') AS album, + CAST(COALESCE( + (SELECT GROUP_CONCAT(g.name, '||') + FROM recording_genres rg_sub + JOIN genres g ON rg_sub.genre_id = g.id + WHERE rg_sub.recording_id = r.id), + '' + ) AS TEXT) AS genre, + COALESCE(r.year, 0) AS year, + COALESCE(r.composer, '') AS composer, + COALESCE(ft.extension, '') AS file_type, + af.sample_rate, + af.bit_depth, + af.channels, + af.bitrate, + af.file_size + FROM audio_files af + LEFT JOIN recordings r ON af.recording_id = r.id + LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id + LEFT JOIN ( + SELECT recording_id, + MIN(release_group_id) AS release_group_id + FROM release_group_recordings + GROUP BY recording_id + ) rgr ON r.id = rgr.recording_id + LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id + LEFT JOIN file_types ft ON af.file_type_id = ft.id + `); err != nil { + return fmt.Errorf( + "migration 4: could not create track_metadata VIEW: %w", + err, + ) + } + + if _, err := db.ExecContext( + ctx, "PRAGMA user_version = 4", + ); err != nil { + return fmt.Errorf( + "could not set user_version to 4: %w", err, + ) + } + + logger.Info("migration 4 complete") + + return nil +} + // isDuplicateColumnErr returns true when the error is SQLite's // "duplicate column name" error from an ALTER TABLE ADD COLUMN // on a column that already exists. diff --git a/backend/database/sql/schemas/track_metadata_view.sql b/backend/database/sql/schemas/track_metadata_view.sql new file mode 100644 index 0000000..68f0879 --- /dev/null +++ b/backend/database/sql/schemas/track_metadata_view.sql @@ -0,0 +1,36 @@ +CREATE VIEW IF NOT EXISTS track_metadata AS +SELECT + af.id, + af.file_path, + af.length_milliseconds, + COALESCE(r.name, '') AS title, + COALESCE(ac.text, '') AS artist_name, + r.track_number, + r.disc_number, + COALESCE(rg.name, '') AS album, + CAST(COALESCE( + (SELECT GROUP_CONCAT(g.name, '||') + FROM recording_genres rg_sub + JOIN genres g ON rg_sub.genre_id = g.id + WHERE rg_sub.recording_id = r.id), + '' + ) AS TEXT) AS genre, + COALESCE(r.year, 0) AS year, + COALESCE(r.composer, '') AS composer, + COALESCE(ft.extension, '') AS file_type, + af.sample_rate, + af.bit_depth, + af.channels, + af.bitrate, + af.file_size +FROM audio_files af +LEFT JOIN recordings r ON af.recording_id = r.id +LEFT JOIN artist_credit ac ON r.artist_credit_id = ac.id +LEFT JOIN ( + SELECT recording_id, + MIN(release_group_id) AS release_group_id + FROM release_group_recordings + GROUP BY recording_id +) rgr ON r.id = rgr.recording_id +LEFT JOIN release_groups rg ON rgr.release_group_id = rg.id +LEFT JOIN file_types ft ON af.file_type_id = ft.id; diff --git a/backend/database/sql/sqlcgen/models.go b/backend/database/sql/sqlcgen/models.go index 1b389f5..6a40a57 100644 --- a/backend/database/sql/sqlcgen/models.go +++ b/backend/database/sql/sqlcgen/models.go @@ -136,3 +136,23 @@ type SearchIndex struct { Artist string Album string } + +type TrackMetadatum struct { + ID int64 + FilePath string + LengthMilliseconds int64 + Title string + ArtistName string + TrackNumber sql.NullInt64 + DiscNumber sql.NullInt64 + Album string + Genre string + Year int64 + Composer string + FileType string + SampleRate int64 + BitDepth int64 + Channels int64 + Bitrate int64 + FileSize int64 +}