feat: sqlite migrations (#398)

- Moved migrations from code to SQL files
- Using golang-migrate/v4
- Added a new CLI command: migrate
This commit is contained in:
Felipe Martin Garcia 2022-03-26 08:33:57 +01:00 committed by GitHub
parent ca3cc11b2d
commit 81d52a2e24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 895 additions and 121 deletions

6
.gitignore vendored
View file

@ -6,9 +6,13 @@
/shiori*
# Exclude development data
/dev-data
/dev-data*
# Coverage data
/coverage.out
# Dist files
dist/
# macOS trash files
.DS_Store

10
go.mod
View file

@ -11,6 +11,7 @@ require (
github.com/go-shiori/warc v0.0.0-20200621032813-359908319d1d
github.com/go-sql-driver/mysql v1.6.0
github.com/gofrs/uuid v4.2.0+incompatible
github.com/golang-migrate/migrate/v4 v4.15.1
github.com/jmoiron/sqlx v1.3.4
github.com/julienschmidt/httprouter v1.3.0
github.com/lib/pq v1.10.4
@ -20,19 +21,19 @@ require (
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.3.0
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
modernc.org/sqlite v1.14.6
)
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/coreos/go-etcd v2.0.0+incompatible // indirect
github.com/cpuguy83/go-md2man v1.0.10 // indirect
github.com/go-shiori/dom v0.0.0-20210627111528-4e4722cd0d65 // indirect
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
@ -40,13 +41,12 @@ require (
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.9 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect

841
go.sum

File diff suppressed because it is too large Load diff

21
internal/cmd/migrate.go Normal file
View file

@ -0,0 +1,21 @@
package cmd
import (
"github.com/spf13/cobra"
)
func migrateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "migrate",
Short: "Migrates the database to the latest version",
Run: migrateHandler,
}
return cmd
}
func migrateHandler(cmd *cobra.Command, args []string) {
if err := db.Migrate(); err != nil {
cError.Printf("Error during migration: %s", err)
}
}

View file

@ -36,6 +36,7 @@ func ShioriCmd() *cobra.Command {
pocketCmd(),
serveCmd(),
checkCmd(),
migrateCmd(),
)
return rootCmd

View file

@ -38,6 +38,9 @@ type GetAccountsOptions struct {
// DB is interface for accessing and manipulating data in database.
type DB interface {
// Migrate runs migrations for this database
Migrate() error
// SaveBookmarks saves bookmarks data to database.
SaveBookmarks(bookmarks ...model.Bookmark) ([]model.Bookmark, error)

View file

@ -0,0 +1,38 @@
CREATE TABLE IF NOT EXISTS account(
id INTEGER NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
owner INTEGER NOT NULL DEFAULT 0,
CONSTRAINT account_PK PRIMARY KEY(id),
CONSTRAINT account_username_UNIQUE UNIQUE(username)
);
CREATE TABLE IF NOT EXISTS bookmark(
id INTEGER NOT NULL,
url TEXT NOT NULL,
title TEXT NOT NULL,
excerpt TEXT NOT NULL DEFAULT "",
author TEXT NOT NULL DEFAULT "",
public INTEGER NOT NULL DEFAULT 0,
modified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT bookmark_PK PRIMARY KEY(id),
CONSTRAINT bookmark_url_UNIQUE UNIQUE(url)
);
CREATE TABLE IF NOT EXISTS tag(
id INTEGER NOT NULL,
name TEXT NOT NULL,
CONSTRAINT tag_PK PRIMARY KEY(id),
CONSTRAINT tag_name_UNIQUE UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS bookmark_tag(
bookmark_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
CONSTRAINT bookmark_tag_PK PRIMARY KEY(bookmark_id, tag_id),
CONSTRAINT bookmark_id_FK FOREIGN KEY(bookmark_id) REFERENCES bookmark(id),
CONSTRAINT tag_id_FK FOREIGN KEY(tag_id) REFERENCES tag(id)
);
CREATE VIRTUAL TABLE IF NOT EXISTS bookmark_content
USING fts5(title, content, html, docid);

View file

@ -92,6 +92,11 @@ func OpenMySQLDatabase(connString string) (mysqlDB *MySQLDatabase, err error) {
return mysqlDB, err
}
// Migrate runs migrations for this database engine
func (db *MySQLDatabase) Migrate() error {
return fmt.Errorf("not implemented")
}
// SaveBookmarks saves new or updated bookmarks to database.
// Returns the saved ID and error message if any happened.
func (db *MySQLDatabase) SaveBookmarks(bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {

View file

@ -87,6 +87,11 @@ func OpenPGDatabase(connString string) (pgDB *PGDatabase, err error) {
return pgDB, err
}
// Migrate runs migrations for this database engine
func (db *PGDatabase) Migrate() error {
return fmt.Errorf("not implemented")
}
// SaveBookmarks saves new or updated bookmarks to database.
// Returns the saved ID and error message if any happened.
func (db *PGDatabase) SaveBookmarks(bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {

View file

@ -2,16 +2,23 @@ package database
import (
"database/sql"
"embed"
"fmt"
"log"
"strings"
"time"
"github.com/go-shiori/shiori/internal/model"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
//go:embed migrations/sqlite/*
var migrations embed.FS
// SQLiteDatabase is implementation of Database interface
// for connecting to SQLite3 database.
type SQLiteDatabase struct {
@ -20,69 +27,32 @@ type SQLiteDatabase struct {
// OpenSQLiteDatabase creates and open connection to new SQLite3 database.
func OpenSQLiteDatabase(databasePath string) (sqliteDB *SQLiteDatabase, err error) {
// Open database and start transaction
// Open database
db := sqlx.MustConnect("sqlite", databasePath)
tx, err := db.Beginx()
if err != nil {
return nil, err
}
// Make sure to rollback if panic ever happened
defer func() {
if r := recover(); r != nil {
panicErr, _ := r.(error)
if err := tx.Rollback(); err != nil {
log.Printf("error during rollback: %s", err)
}
sqliteDB = nil
err = panicErr
}
}()
// Create tables
tx.MustExec(`CREATE TABLE IF NOT EXISTS account(
id INTEGER NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
owner INTEGER NOT NULL DEFAULT 0,
CONSTRAINT account_PK PRIMARY KEY(id),
CONSTRAINT account_username_UNIQUE UNIQUE(username))`)
tx.MustExec(`CREATE TABLE IF NOT EXISTS bookmark(
id INTEGER NOT NULL,
url TEXT NOT NULL,
title TEXT NOT NULL,
excerpt TEXT NOT NULL DEFAULT "",
author TEXT NOT NULL DEFAULT "",
public INTEGER NOT NULL DEFAULT 0,
modified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT bookmark_PK PRIMARY KEY(id),
CONSTRAINT bookmark_url_UNIQUE UNIQUE(url))`)
tx.MustExec(`CREATE TABLE IF NOT EXISTS tag(
id INTEGER NOT NULL,
name TEXT NOT NULL,
CONSTRAINT tag_PK PRIMARY KEY(id),
CONSTRAINT tag_name_UNIQUE UNIQUE(name))`)
tx.MustExec(`CREATE TABLE IF NOT EXISTS bookmark_tag(
bookmark_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
CONSTRAINT bookmark_tag_PK PRIMARY KEY(bookmark_id, tag_id),
CONSTRAINT bookmark_id_FK FOREIGN KEY(bookmark_id) REFERENCES bookmark(id),
CONSTRAINT tag_id_FK FOREIGN KEY(tag_id) REFERENCES tag(id))`)
tx.MustExec(`CREATE VIRTUAL TABLE IF NOT EXISTS bookmark_content
USING fts5(title, content, html, docid)`)
err = tx.Commit()
checkError(err)
sqliteDB = &SQLiteDatabase{*db}
return sqliteDB, err
}
// Migrate runs migrations for this database engine
func (db *SQLiteDatabase) Migrate() error {
sourceDriver, err := iofs.New(migrations, "migrations/sqlite")
checkError(err)
dbDriver, err := sqlite.WithInstance(db.DB.DB, &sqlite.Config{})
checkError(err)
migration, err := migrate.NewWithInstance(
"iofs",
sourceDriver,
"sqlite",
dbDriver,
)
checkError(err)
return migration.Up()
}
// SaveBookmarks saves new or updated bookmarks to database.
// Returns the saved ID and error message if any happened.
func (db *SQLiteDatabase) SaveBookmarks(bookmarks ...model.Bookmark) (result []model.Bookmark, err error) {