lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

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 }