Change own DbTable implementation for Gorm
This commit is contained in:
@@ -1,57 +1,21 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Chapter struct {
|
||||
Id int
|
||||
MangaId int
|
||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||
Url string
|
||||
Name string
|
||||
Number string
|
||||
TimeStampUnix int64
|
||||
MangaId int
|
||||
}
|
||||
|
||||
func NewChapter(id int, mangaId int, url string, name string, number string, timeStampUnix int64) Chapter {
|
||||
return Chapter{
|
||||
Id: id,
|
||||
MangaId: mangaId,
|
||||
Url: url,
|
||||
Name: name,
|
||||
Number: number,
|
||||
TimeStampUnix: timeStampUnix,
|
||||
MangaId: mangaId,
|
||||
}
|
||||
}
|
||||
|
||||
func updateChapter(db *sql.DB, c *Chapter) error {
|
||||
_, err := db.Exec("UPDATE Chapter set Name = ?, Url = ?, Number = ?, TimeStampUnixEpoch = ? where ID = ?", c.Name, c.Url, c.Number, c.TimeStampUnix, c.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
func insertChapter(db *sql.DB, c *Chapter) error {
|
||||
_, err := db.Exec("INSERT INTO Chapter(ID, MangaID, Url, Name, Number, TimeStampUnixEpoch) VALUES (?, ?, ?, ?, ?, ?)", c.Id, c.MangaId, c.Url, c.Name, c.Number, c.TimeStampUnix)
|
||||
return err
|
||||
}
|
||||
|
||||
func loadChapters(db *sql.DB) (map[int]Chapter, error) {
|
||||
rows, err := db.Query("SELECT Id, MangaID, Url, Name, Number, TimeStampUnixEpoch FROM Chapter")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[int]Chapter)
|
||||
|
||||
for rows.Next() {
|
||||
chapter := Chapter{}
|
||||
if err = rows.Scan(&chapter.Id, &chapter.MangaId, &chapter.Url, &chapter.Name, &chapter.Number, &chapter.TimeStampUnix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res[chapter.Id] = chapter
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func deleteChapter(db *sql.DB, key int) error {
|
||||
_, err := db.Exec("DELETE from Chapter where ID = ?", key)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
create table if not exists Manga (
|
||||
ID integer not null primary key,
|
||||
Title text,
|
||||
TimeStampUnixEpoch integer not null,
|
||||
Thumbnail blob null,
|
||||
LatestAvailableChapter text
|
||||
);
|
||||
|
||||
create table if not exists Chapter (
|
||||
ID integer not null primary key,
|
||||
MangaID integer not null,
|
||||
Url text not null,
|
||||
Name text null,
|
||||
Number text null,
|
||||
TimeStampUnixEpoch integer not null,
|
||||
foreign key(MangaID) references Manga(ID)
|
||||
);
|
||||
|
||||
create table if not exists Setting (
|
||||
Name text not null primary key,
|
||||
Value text,
|
||||
DefaultValue text not null
|
||||
);
|
||||
@@ -1,115 +1,59 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"errors"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
ConnectionString string
|
||||
db *sql.DB
|
||||
Mangas DbTable[int, Manga]
|
||||
Chapters DbTable[int, Chapter]
|
||||
Settings DbTable[string, Setting]
|
||||
Db *gorm.DB
|
||||
CreateIfNotExists bool
|
||||
}
|
||||
|
||||
func NewDatabase(connectionString string, createIfNotExists bool) Manager {
|
||||
return Manager{
|
||||
ConnectionString: connectionString,
|
||||
db: nil,
|
||||
Mangas: NewDbTable(updateManga, insertManga, loadMangas, deleteManga),
|
||||
Chapters: NewDbTable(updateChapter, insertChapter, loadChapters, deleteChapter),
|
||||
Settings: NewDbTable(updateSetting, insertSetting, loadSettings, deleteSetting),
|
||||
Db: nil,
|
||||
CreateIfNotExists: createIfNotExists,
|
||||
}
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) Open() error {
|
||||
db, err := sql.Open("sqlite3", dbMgr.ConnectionString)
|
||||
db, err := gorm.Open(sqlite.Open(dbMgr.ConnectionString), &gorm.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbMgr.db = db
|
||||
dbMgr.Db = db
|
||||
if dbMgr.CreateIfNotExists {
|
||||
err = dbMgr.createDatabaseIfNotExists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = dbMgr.load()
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) Close() error {
|
||||
err := dbMgr.db.Close()
|
||||
sql, err := dbMgr.Db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbMgr.db = nil
|
||||
err = sql.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbMgr.Db = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) Delete(mangaId int) error {
|
||||
dbMgr.Mangas.Get(mangaId)
|
||||
err := dbMgr.Mangas.Delete(dbMgr.db, mangaId)
|
||||
if err != nil && !errors.Is(err, IgnoreDeleteError{}) {
|
||||
return err
|
||||
}
|
||||
|
||||
chapters := dbMgr.Chapters.All()
|
||||
for i, chapter := range chapters {
|
||||
if chapter.MangaId == mangaId {
|
||||
err := dbMgr.Chapters.Delete(dbMgr.db, i)
|
||||
if err != nil && !errors.Is(err, IgnoreDeleteError{}) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (dbMgr *Manager) Delete(mangaId int) {
|
||||
dbMgr.Db.Delete(&Manga{}, mangaId)
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) Save() error {
|
||||
err := dbMgr.Mangas.Save(dbMgr.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = dbMgr.Chapters.Save(dbMgr.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dbMgr.Settings.Save(dbMgr.db)
|
||||
}
|
||||
|
||||
//go:embed createDb.sql
|
||||
var createSql string
|
||||
|
||||
func (dbMgr *Manager) createDatabaseIfNotExists() error {
|
||||
_, err := dbMgr.db.Exec(createSql)
|
||||
err := dbMgr.Db.AutoMigrate(&Manga{}, &Chapter{}, &Setting{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (dbMgr *Manager) load() error {
|
||||
err := dbMgr.Chapters.Load(dbMgr.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dbMgr.Mangas.Load(dbMgr.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dbMgr.Settings.Load(dbMgr.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initSettings(&dbMgr.Settings)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Manga struct {
|
||||
Id int
|
||||
Id int `gorm:"primary_key;AUTO_INCREMENT"`
|
||||
Title string
|
||||
TimeStampUnix int64
|
||||
Thumbnail *bytes.Buffer
|
||||
Thumbnail []byte
|
||||
LastChapterNum string
|
||||
Chapters []Chapter
|
||||
//`gorm:"foreignkey:MangaID"`
|
||||
}
|
||||
|
||||
func NewManga(id int, title string, timeStampUnix int64) Manga {
|
||||
@@ -23,12 +20,10 @@ func NewManga(id int, title string, timeStampUnix int64) Manga {
|
||||
}
|
||||
|
||||
// GetLatestChapter TODO: Cache this somehow
|
||||
func (m *Manga) GetLatestChapter(chapters *DbTable[int, Chapter]) (*Chapter, bool) {
|
||||
c := chapters.All()
|
||||
|
||||
func (m *Manga) GetLatestChapter() (*Chapter, bool) {
|
||||
highest := int64(0)
|
||||
index := 0
|
||||
for i, chapter := range c {
|
||||
for i, chapter := range m.Chapters {
|
||||
if chapter.MangaId == m.Id && highest < chapter.TimeStampUnix {
|
||||
highest = chapter.TimeStampUnix
|
||||
index = i
|
||||
@@ -39,58 +34,14 @@ func (m *Manga) GetLatestChapter(chapters *DbTable[int, Chapter]) (*Chapter, boo
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &c[index], true
|
||||
}
|
||||
|
||||
func updateManga(db *sql.DB, m *Manga) error {
|
||||
const cmd = "UPDATE Manga set Title = ?, TimeStampUnixEpoch = ?, Thumbnail = ?, LatestAvailableChapter = ? WHERE ID = ?"
|
||||
var err error
|
||||
if m.Thumbnail == nil {
|
||||
_, err = db.Exec(cmd, m.Title, m.TimeStampUnix, nil, m.LastChapterNum, m.Id)
|
||||
|
||||
} else {
|
||||
_, err = db.Exec(cmd, m.Title, m.TimeStampUnix, m.Thumbnail.Bytes(), m.LastChapterNum, m.Id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func insertManga(db *sql.DB, manga *Manga) error {
|
||||
const cmd = "INSERT INTO Manga(ID, Title, TimeStampUnixEpoch, Thumbnail, LatestAvailableChapter) values(?, ?, ?, ?, ?)"
|
||||
var err error
|
||||
if manga.Thumbnail == nil {
|
||||
_, err = db.Exec(cmd, manga.Id, manga.Title, manga.TimeStampUnix, nil, manga.LastChapterNum)
|
||||
|
||||
} else {
|
||||
_, err = db.Exec(cmd, manga.Id, manga.Title, manga.TimeStampUnix, manga.Thumbnail.Bytes(), manga.LastChapterNum)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func loadMangas(db *sql.DB) (map[int]Manga, error) {
|
||||
rows, err := db.Query("SELECT Id, Title, TimeStampUnixEpoch, Thumbnail, LatestAvailableChapter FROM Manga")
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[int]Manga)
|
||||
|
||||
for rows.Next() {
|
||||
manga := Manga{}
|
||||
var thumbnail []byte
|
||||
if err = rows.Scan(&manga.Id, &manga.Title, &manga.TimeStampUnix, &thumbnail, &manga.LastChapterNum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(thumbnail) != 0 {
|
||||
manga.Thumbnail = bytes.NewBuffer(thumbnail)
|
||||
}
|
||||
|
||||
res[manga.Id] = manga
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func deleteManga(db *sql.DB, key int) error {
|
||||
_, err := db.Exec("DELETE from Manga where ID = ?", key)
|
||||
return err
|
||||
return &m.Chapters[index], true
|
||||
|
||||
//result := db.Where("manga.id = ?", m.Id).Order("TimeStampUnix desc").Take(&chapter)
|
||||
//if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
// return &chapter, true, result.Error
|
||||
//} else if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
// return &chapter, false, nil
|
||||
//} else {
|
||||
// return &chapter, true, nil
|
||||
//}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Name string
|
||||
Name string `gorm:"PRIMARY_KEY"`
|
||||
Value string
|
||||
Default string
|
||||
}
|
||||
@@ -18,57 +14,14 @@ func NewSetting(name string, defaultValue string) Setting {
|
||||
}
|
||||
}
|
||||
|
||||
func initSettings(settings *DbTable[string, Setting]) {
|
||||
addSettingIfNotExists("theme", "white", settings)
|
||||
addSettingIfNotExists("order", "title", settings)
|
||||
}
|
||||
|
||||
func addSettingIfNotExists(name string, value string, settings *DbTable[string, Setting]) {
|
||||
_, exists := settings.Get(name)
|
||||
if !exists {
|
||||
settings.Set(name, NewSetting(name, value))
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(db *sql.DB, s *Setting) error {
|
||||
const cmd = "UPDATE Setting set Value = ? WHERE Name = ?"
|
||||
_, err := db.Exec(cmd, s.Value, s.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func insertSetting(db *sql.DB, s *Setting) error {
|
||||
const cmd = "INSERT INTO Setting(Name, Value, DefaultValue) VALUES(?, ?, ?)"
|
||||
_, err := db.Exec(cmd, s.Name, s.Value, s.Default)
|
||||
return err
|
||||
}
|
||||
|
||||
func loadSettings(db *sql.DB) (map[string]Setting, error) {
|
||||
const cmd = "SELECT Name, Value, DefaultValue from Setting"
|
||||
rows, err := db.Query(cmd)
|
||||
|
||||
res := make(map[string]Setting)
|
||||
|
||||
for rows.Next() {
|
||||
setting := Setting{}
|
||||
if err = rows.Scan(&setting.Name, &setting.Value, &setting.Default); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res[setting.Name] = setting
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
type IgnoreDeleteError struct{}
|
||||
|
||||
func (m IgnoreDeleteError) Error() string {
|
||||
return "Should ignore deletion"
|
||||
}
|
||||
|
||||
func deleteSetting(db *sql.DB, key string) error {
|
||||
const cmd = "UPDATE Setting set Value = DefaultValue WHERE Name = ?"
|
||||
_, err := db.Exec(cmd, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return IgnoreDeleteError{}
|
||||
}
|
||||
//func initSettings(settings *DbTable[string, Setting]) {
|
||||
// addSettingIfNotExists("theme", "white", settings)
|
||||
// addSettingIfNotExists("order", "title", settings)
|
||||
//}
|
||||
//
|
||||
//func addSettingIfNotExists(name string, value string, settings *DbTable[string, Setting]) {
|
||||
// _, exists := settings.Get(name)
|
||||
// if !exists {
|
||||
// settings.Set(name, NewSetting(name, value))
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DbStatus uint8
|
||||
|
||||
const (
|
||||
New DbStatus = iota
|
||||
Loaded
|
||||
Updated
|
||||
)
|
||||
|
||||
type DbTable[K comparable, T any] struct {
|
||||
mutex sync.Mutex
|
||||
items map[K]T
|
||||
updated map[K]DbStatus
|
||||
updateFunc func(db *sql.DB, value *T) error
|
||||
insertFunc func(db *sql.DB, value *T) error
|
||||
loadFunc func(db *sql.DB) (map[K]T, error)
|
||||
deleteFunc func(db *sql.DB, key K) error
|
||||
}
|
||||
|
||||
func NewDbTable[K comparable, T any](updateFunc func(db *sql.DB, value *T) error,
|
||||
insertFunc func(db *sql.DB, value *T) error,
|
||||
loadFunc func(db *sql.DB) (map[K]T, error),
|
||||
deleteFunc func(db *sql.DB, key K) error,
|
||||
) DbTable[K, T] {
|
||||
return DbTable[K, T]{
|
||||
mutex: sync.Mutex{},
|
||||
items: make(map[K]T),
|
||||
updated: make(map[K]DbStatus),
|
||||
updateFunc: updateFunc,
|
||||
insertFunc: insertFunc,
|
||||
loadFunc: loadFunc,
|
||||
deleteFunc: deleteFunc,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Get(key K) (T, bool) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
val, ok := d.items[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Set(key K, new T) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
val, ok := d.updated[key]
|
||||
if ok && val == Loaded || val == Updated {
|
||||
d.updated[key] = Updated
|
||||
} else {
|
||||
d.updated[key] = New
|
||||
}
|
||||
d.items[key] = new
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) All() []T {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
res := make([]T, len(d.items))
|
||||
counter := 0
|
||||
for _, manga := range d.items {
|
||||
res[counter] = manga
|
||||
counter++
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Map() map[K]T {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
res := make(map[K]T, len(d.items))
|
||||
for k, manga := range d.items {
|
||||
res[k] = manga
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) First(filter func(match T) bool) (key K, value T, ok bool) {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
|
||||
for k, manga := range d.items {
|
||||
if filter(manga) {
|
||||
return k, manga, true
|
||||
}
|
||||
}
|
||||
|
||||
return *new(K), *new(T), false
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Where(filter func(match T) bool) map[K]T {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
res := make(map[K]T, len(d.items))
|
||||
for k, manga := range d.items {
|
||||
if filter(manga) {
|
||||
res[k] = manga
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Delete(db *sql.DB, key K) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
err := d.deleteFunc(db, key)
|
||||
if err == nil {
|
||||
delete(d.items, key)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Save(db *sql.DB) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
for k, status := range d.updated {
|
||||
switch status {
|
||||
case Loaded:
|
||||
continue
|
||||
case Updated:
|
||||
item := d.items[k]
|
||||
err := d.updateFunc(db, &item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.updated[k] = Loaded
|
||||
case New:
|
||||
item := d.items[k]
|
||||
err := d.insertFunc(db, &item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.updated[k] = Loaded
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DbTable[K, T]) Load(db *sql.DB) error {
|
||||
d.mutex.Lock()
|
||||
defer d.mutex.Unlock()
|
||||
res, err := d.loadFunc(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.items = res
|
||||
for k := range d.items {
|
||||
d.updated[k] = Loaded
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user