2021-06-26 18:08:35 +00:00
|
|
|
package database
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"embed"
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.yetaga.in/alazyreader/library/book"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
|
|
)
|
|
|
|
|
|
|
|
//go:embed migrations/mysql
|
|
|
|
var migrationsFS embed.FS
|
|
|
|
|
|
|
|
type migration struct {
|
|
|
|
id int
|
|
|
|
name string
|
|
|
|
content []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
type MySQL struct {
|
|
|
|
connection *sql.DB
|
2021-07-02 22:13:58 +00:00
|
|
|
tableName string
|
2021-06-26 18:08:35 +00:00
|
|
|
versionTable string
|
|
|
|
migrationsDirectory string
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewMySQLConnection(user, pass, host, port, db string) (*MySQL, error) {
|
|
|
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, host, port, db) // what a strange syntax
|
|
|
|
connection, err := sql.Open("mysql", dsn)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &MySQL{
|
|
|
|
connection: connection,
|
2021-07-02 22:13:58 +00:00
|
|
|
tableName: "books",
|
2021-06-26 18:08:35 +00:00
|
|
|
versionTable: "migrations",
|
2021-07-02 22:13:58 +00:00
|
|
|
migrationsDirectory: "migrations/mysql",
|
2021-06-26 18:08:35 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MySQL) PrepareDatabase(ctx context.Context) error {
|
|
|
|
if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" {
|
|
|
|
return fmt.Errorf("uninitialized mysql client")
|
|
|
|
}
|
|
|
|
|
|
|
|
tablecheck := `SELECT count(*) AS count
|
|
|
|
FROM information_schema.TABLES
|
|
|
|
WHERE TABLE_NAME = '` + m.versionTable + `'
|
|
|
|
AND TABLE_SCHEMA in (SELECT DATABASE());`
|
|
|
|
tableschema := `CREATE TABLE ` + m.versionTable + `(
|
|
|
|
id INT NOT NULL,
|
|
|
|
name VARCHAR(100) NOT NULL,
|
|
|
|
datetime DATE,
|
|
|
|
PRIMARY KEY (id))`
|
|
|
|
|
|
|
|
var versionTableExists int
|
|
|
|
m.connection.QueryRowContext(ctx, tablecheck).Scan(&versionTableExists)
|
|
|
|
if versionTableExists != 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
_, err := m.connection.ExecContext(ctx, tableschema)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MySQL) GetLatestMigration(ctx context.Context) (int, error) {
|
|
|
|
if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" {
|
|
|
|
return 0, fmt.Errorf("uninitialized mysql client")
|
|
|
|
}
|
|
|
|
|
|
|
|
var latestMigration int
|
2021-07-02 22:13:58 +00:00
|
|
|
err := m.connection.QueryRowContext(ctx, "SELECT COALESCE(MAX(id), 0) FROM "+m.versionTable).Scan(&latestMigration)
|
2021-06-26 18:08:35 +00:00
|
|
|
return latestMigration, err
|
|
|
|
}
|
|
|
|
|
2021-07-02 22:13:58 +00:00
|
|
|
func (m *MySQL) RunMigrations(ctx context.Context) (int, int, error) {
|
2021-06-26 18:08:35 +00:00
|
|
|
if m.connection == nil || m.migrationsDirectory == "" || m.versionTable == "" {
|
2021-07-02 22:13:58 +00:00
|
|
|
return 0, 0, fmt.Errorf("uninitialized mysql client")
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
|
2021-07-02 22:13:58 +00:00
|
|
|
migrations := map[int]migration{}
|
2021-06-26 18:08:35 +00:00
|
|
|
dir, err := migrationsFS.ReadDir(m.migrationsDirectory)
|
|
|
|
if err != nil {
|
2021-07-02 22:13:58 +00:00
|
|
|
return 0, 0, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
for f := range dir {
|
|
|
|
if dir[f].Type().IsRegular() {
|
|
|
|
mig := migration{}
|
|
|
|
id, name, err := parseMigrationFileName(dir[f].Name())
|
|
|
|
if err != nil {
|
2021-07-02 22:13:58 +00:00
|
|
|
return 0, 0, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
mig.id, mig.name = id, name
|
|
|
|
mig.content, err = fs.ReadFile(migrationsFS, m.migrationsDirectory+"/"+dir[f].Name())
|
|
|
|
migrations[mig.id] = mig
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
latestMigrationRan, err := m.GetLatestMigration(ctx)
|
|
|
|
if err != nil {
|
2021-07-02 22:13:58 +00:00
|
|
|
return 0, 0, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// exit if nothing to do (that is, there's no greater migration ID)
|
|
|
|
if _, ok := migrations[latestMigrationRan+1]; !ok {
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, 0, nil
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// loop over and apply migrations if required
|
|
|
|
tx, err := m.connection.BeginTx(ctx, nil)
|
|
|
|
if err != nil {
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, 0, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
2021-07-02 22:13:58 +00:00
|
|
|
migrationsRun := 0
|
2021-06-26 18:08:35 +00:00
|
|
|
for migrationsToRun := true; migrationsToRun; _, migrationsToRun = migrations[latestMigrationRan+1] {
|
|
|
|
mig := migrations[latestMigrationRan+1]
|
|
|
|
_, err := tx.ExecContext(ctx, string(mig.content))
|
|
|
|
if err != nil {
|
|
|
|
nestederr := tx.Rollback()
|
|
|
|
if nestederr != nil {
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, migrationsRun, nestederr
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, migrationsRun, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, "INSERT INTO "+m.versionTable+" (id, name, datetime) VALUES (?, ?, ?)", mig.id, mig.name, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
nestederr := tx.Rollback()
|
|
|
|
if nestederr != nil {
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, migrationsRun, nestederr
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, migrationsRun, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
latestMigrationRan = latestMigrationRan + 1
|
2021-07-02 22:13:58 +00:00
|
|
|
migrationsRun = migrationsRun + 1
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
err = tx.Commit()
|
2021-07-02 22:13:58 +00:00
|
|
|
return latestMigrationRan, migrationsRun, err
|
2021-06-26 18:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MySQL) GetAllBooks(ctx context.Context) ([]book.Book, error) {
|
|
|
|
if m.connection == nil {
|
|
|
|
return nil, fmt.Errorf("uninitialized mysql client")
|
|
|
|
}
|
|
|
|
|
|
|
|
books := []book.Book{}
|
2021-07-02 22:13:58 +00:00
|
|
|
rows, err := m.connection.QueryContext(ctx, `
|
|
|
|
SELECT id,
|
|
|
|
title,
|
|
|
|
authors,
|
|
|
|
sortauthor,
|
|
|
|
isbn10,
|
|
|
|
isbn13,
|
|
|
|
format,
|
|
|
|
genre,
|
|
|
|
publisher,
|
|
|
|
series,
|
|
|
|
volume,
|
|
|
|
year,
|
|
|
|
signed,
|
|
|
|
description,
|
|
|
|
notes,
|
|
|
|
onloan,
|
|
|
|
coverurl
|
|
|
|
FROM `+m.tableName)
|
2021-06-26 18:08:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
b := book.Book{}
|
2021-07-02 22:13:58 +00:00
|
|
|
var authors string
|
|
|
|
err := rows.Scan(
|
|
|
|
&b.ID, &b.Title, &authors,
|
|
|
|
&b.SortAuthor, &b.ISBN10, &b.ISBN13,
|
|
|
|
&b.Format, &b.Genre, &b.Publisher,
|
|
|
|
&b.Series, &b.Volume, &b.Year,
|
|
|
|
&b.Signed, &b.Description, &b.Notes,
|
|
|
|
&b.OnLoan, &b.CoverURL)
|
2021-06-26 18:08:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-02 22:13:58 +00:00
|
|
|
b.Authors = strings.Split(authors, ";")
|
2021-06-26 18:08:35 +00:00
|
|
|
books = append(books, b)
|
|
|
|
}
|
|
|
|
|
|
|
|
return books, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MySQL) AddBook(ctx context.Context, b *book.Book) error {
|
|
|
|
if m.connection == nil {
|
|
|
|
return fmt.Errorf("uninitialized mysql client")
|
|
|
|
}
|
|
|
|
|
2021-07-02 22:13:58 +00:00
|
|
|
res, err := m.connection.ExecContext(ctx, `
|
|
|
|
INSERT INTO `+m.tableName+`
|
|
|
|
(title, authors, sortauthor, isbn10, isbn13, format, genre, publisher, series, volume, year, signed, description, notes, onloan, coverurl)
|
|
|
|
VALUES
|
2021-08-01 22:50:53 +00:00
|
|
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
2021-07-02 22:13:58 +00:00
|
|
|
b.Title,
|
|
|
|
strings.Join(b.Authors, ";"),
|
|
|
|
b.SortAuthor,
|
|
|
|
b.ISBN10,
|
|
|
|
b.ISBN13,
|
|
|
|
b.Format,
|
|
|
|
b.Genre,
|
|
|
|
b.Publisher,
|
|
|
|
b.Series,
|
|
|
|
b.Volume,
|
|
|
|
b.Year,
|
|
|
|
b.Signed,
|
|
|
|
b.Description,
|
|
|
|
b.Notes,
|
|
|
|
b.OnLoan,
|
|
|
|
b.CoverURL,
|
|
|
|
)
|
2021-06-26 18:08:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
i, err := res.RowsAffected()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if i != 1 {
|
|
|
|
return fmt.Errorf("unexpectedly updated more than one row: %d", i)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *MySQL) UpdateBook(ctx context.Context, old, new *book.Book) error {
|
|
|
|
if m.connection == nil {
|
|
|
|
return fmt.Errorf("uninitialized mysql client")
|
|
|
|
}
|
2021-07-02 22:13:58 +00:00
|
|
|
if old.ID != new.ID {
|
|
|
|
return fmt.Errorf("cannot change book ID")
|
|
|
|
}
|
2021-06-26 18:08:35 +00:00
|
|
|
|
2021-07-02 22:13:58 +00:00
|
|
|
res, err := m.connection.ExecContext(ctx, `
|
|
|
|
UPDATE `+m.tableName+`
|
|
|
|
SET id=?
|
|
|
|
title=?
|
|
|
|
authors=?
|
|
|
|
sortauthor=?
|
|
|
|
isbn10=?
|
|
|
|
isbn13=?
|
|
|
|
format=?
|
|
|
|
genre=?
|
|
|
|
publisher=?
|
|
|
|
series=?
|
|
|
|
volume=?
|
|
|
|
year=?
|
|
|
|
signed=?
|
|
|
|
description=?
|
|
|
|
notes=?
|
|
|
|
onloan=?
|
|
|
|
coverurl=?
|
|
|
|
WHERE id=?`,
|
|
|
|
new.Title,
|
|
|
|
strings.Join(new.Authors, ";"),
|
|
|
|
new.SortAuthor,
|
|
|
|
new.ISBN10,
|
|
|
|
new.ISBN13,
|
|
|
|
new.Format,
|
|
|
|
new.Genre,
|
|
|
|
new.Publisher,
|
|
|
|
new.Series,
|
|
|
|
new.Volume,
|
|
|
|
new.Year,
|
|
|
|
new.Signed,
|
|
|
|
new.Description,
|
|
|
|
new.Notes,
|
|
|
|
new.OnLoan,
|
|
|
|
new.CoverURL,
|
|
|
|
old.ID)
|
2021-06-26 18:08:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
i, err := res.RowsAffected()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if i != 1 {
|
|
|
|
return fmt.Errorf("unexpectedly updated more than one row: %d", i)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseMigrationFileName(filename string) (int, string, error) {
|
|
|
|
sp := strings.SplitN(filename, "-", 2)
|
|
|
|
i, err := strconv.Atoi(sp[0])
|
|
|
|
if err != nil {
|
|
|
|
return 0, "", err
|
|
|
|
}
|
|
|
|
tr := strings.TrimSuffix(sp[1], ".sql")
|
|
|
|
return i, tr, nil
|
|
|
|
}
|