db.go (4781B)
1 package db 2 3 import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "log/slog" 8 "lyrics/internal/log" 9 "strings" 10 11 _ "github.com/mattn/go-sqlite3" 12 ) 13 14 type DB struct { 15 *sql.DB 16 logger *slog.Logger 17 } 18 19 type Execer interface { 20 Query(query string, args ...any) (*sql.Rows, error) 21 QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) 22 QueryRow(query string, args ...any) *sql.Row 23 QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row 24 Exec(query string, args ...any) (sql.Result, error) 25 ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) 26 Prepare(query string) (*sql.Stmt, error) 27 PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 28 } 29 30 func New(ctx context.Context, dbPath string) (*DB, error) { 31 // https://github.com/mattn/go-sqlite3#connection-string 32 opts := []string{ 33 "_foreign_keys=1", 34 "_journal_mode=WAL", 35 "_synchronous=NORMAL", 36 "_auto_vacuum=incremental", 37 } 38 39 logger := log.FromContext(ctx) 40 logger = log.SubLogger(logger, "db") 41 42 db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 43 if err != nil { 44 return nil, fmt.Errorf("failed to open db: %w", err) 45 } 46 47 conn, err := db.Conn(ctx) 48 if err != nil { 49 return nil, err 50 } 51 defer conn.Close() 52 53 logger.Debug("creating tables") 54 55 tx, err := conn.BeginTx(ctx, nil) 56 if err != nil { 57 return nil, err 58 } 59 defer tx.Rollback() 60 61 _, err = tx.ExecContext(ctx, ` 62 create table if not exists songs ( 63 id integer primary key autoincrement, 64 musicbrainz_recording_id text, 65 title text not null, 66 latin_title text, 67 lyrics text not null, 68 release_date text not null, 69 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 70 ); 71 `) 72 if err != nil { 73 return nil, fmt.Errorf("failed to create songs table: %w", err) 74 } 75 76 _, err = tx.ExecContext(ctx, ` 77 create table if not exists artists ( 78 id integer primary key autoincrement, 79 musicbrainz_artist_id text, 80 name text not null, 81 latin_name text, 82 disambiguation text, 83 bio text, 84 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 85 ); 86 `) 87 if err != nil { 88 return nil, fmt.Errorf("failed to create artists table: %w", err) 89 } 90 91 _, err = tx.ExecContext(ctx, ` 92 create table if not exists releases ( 93 id integer primary key autoincrement, 94 musicbrainz_release_id text, 95 title text not null, 96 latin_title text, 97 release_date text not null, 98 -- album, single, ep, other 99 type text not null, 100 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 101 102 unique(title) 103 ); 104 `) 105 if err != nil { 106 return nil, fmt.Errorf("failed to create releases table: %w", err) 107 } 108 109 _, err = tx.ExecContext(ctx, ` 110 create table if not exists release_artists ( 111 release_id integer not null, 112 artist_id integer not null, 113 role text not null, 114 sort_order integer not null, 115 116 unique(release_id, sort_order), 117 foreign key (release_id) references releases(id) on delete cascade, 118 foreign key (artist_id) references artists(id) on delete cascade, 119 primary key (release_id, artist_id, role) 120 ); 121 `) 122 if err != nil { 123 return nil, fmt.Errorf("failed to create release_artists table: %w", err) 124 } 125 126 _, err = tx.ExecContext(ctx, ` 127 create table if not exists song_artists ( 128 song_id integer not null, 129 artist_id integer not null, 130 role text not null, 131 sort_order integer not null, 132 133 unique(song_id, sort_order), 134 unique(song_id, role), 135 foreign key (song_id) references songs(id) on delete cascade, 136 foreign key (artist_id) references artists(id) on delete cascade, 137 primary key (song_id, artist_id, role) 138 ); 139 `) 140 if err != nil { 141 return nil, fmt.Errorf("failed to create song_artists table: %w", err) 142 } 143 144 _, err = tx.ExecContext(ctx, ` 145 create table if not exists song_genres ( 146 song_id integer not null, 147 genre text not null, 148 149 foreign key (song_id) references songs(id) on delete cascade, 150 primary key (song_id, genre) 151 ); 152 `) 153 if err != nil { 154 return nil, fmt.Errorf("failed to create song_genres table: %w", err) 155 } 156 157 _, err = tx.ExecContext(ctx, ` 158 create table if not exists song_releases ( 159 song_id integer not null, 160 release_id integer not null, 161 track_number integer not null, 162 163 unique(release_id, track_number), 164 foreign key (song_id) references songs(id) on delete cascade, 165 foreign key (release_id) references releases(id) on delete cascade, 166 primary key (song_id, release_id) 167 ); 168 `) 169 if err != nil { 170 return nil, fmt.Errorf("failed to create song_releases table: %w", err) 171 } 172 173 if err = tx.Commit(); err != nil { 174 return nil, fmt.Errorf("failed to commit db create operations: %w", err) 175 } 176 177 logger.Debug("successfully created tables") 178 179 return &DB{db, logger}, nil 180 } 181 182 func (d *DB) Close() error { 183 return d.DB.Close() 184 }