261 lines
5.4 KiB
Go
261 lines
5.4 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
sq "github.com/Masterminds/squirrel"
|
|
"github.com/abelith/spellbook/internal/errors"
|
|
"github.com/abelith/spellbook/internal/models"
|
|
"strings"
|
|
)
|
|
|
|
const schema = `
|
|
CREATE TABLE IF NOT EXISTS spells (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
spell TEXT NOT NULL,
|
|
short_desc TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
tags TEXT NOT NULL DEFAULT ''
|
|
);
|
|
`
|
|
|
|
type scanner interface {
|
|
Scan(dst ...any) error
|
|
}
|
|
|
|
type Repo struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewRepo(db *sql.DB) *Repo {
|
|
return &Repo{db: db}
|
|
}
|
|
|
|
func (r *Repo) Init(ctx context.Context) error {
|
|
if r.db == nil {
|
|
return errors.ErrNoDBProvided
|
|
}
|
|
|
|
if _, err := r.db.ExecContext(ctx, schema); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Repo) Add(ctx context.Context, spell models.Spell) error {
|
|
if _, err := insertSpell(ctx, r.db, spell); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Repo) Delete(ctx context.Context, spellName string) error {
|
|
_, err := sq.Delete("spells").
|
|
Where(sq.Eq{"name": spellName}).
|
|
PlaceholderFormat(sq.Question).
|
|
RunWith(r.db).
|
|
ExecContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Repo) Get(ctx context.Context, spellName string) (models.Spell, error) {
|
|
row := sq.Select("id", "name", "spell", "short_desc", "description", "tags").
|
|
From("spells").
|
|
Where(sq.Eq{"name": spellName}).
|
|
PlaceholderFormat(sq.Question).
|
|
RunWith(r.db).
|
|
QueryRowContext(ctx)
|
|
|
|
return scanSpell(row)
|
|
}
|
|
|
|
func (r *Repo) GetMany(ctx context.Context, query models.SearchQuery) ([]models.Spell, error) {
|
|
builder := sq.Select("id", "name", "spell", "short_desc", "description", "tags").
|
|
From("spells")
|
|
|
|
if query.Cursor > 0 {
|
|
builder = builder.Where(sq.Gt{"id": query.Cursor})
|
|
}
|
|
|
|
if query.Text != "" {
|
|
pattern := "%" + query.Text + "%"
|
|
builder = builder.Where(sq.Or{
|
|
sq.Like{"name": pattern},
|
|
sq.Like{"spell": pattern},
|
|
sq.Like{"description": pattern},
|
|
})
|
|
}
|
|
|
|
if query.Name != "" {
|
|
builder = builder.Where(sq.Like{"name": "%" + query.Name + "%"})
|
|
}
|
|
if query.Command != "" {
|
|
builder = builder.Where(sq.Like{"spell": "%" + query.Command + "%"})
|
|
}
|
|
if query.Description != "" {
|
|
builder = builder.Where(sq.Like{"description": "%" + query.Description + "%"})
|
|
}
|
|
|
|
if len(query.Tags) > 0 {
|
|
var tagConditions sq.Or
|
|
for _, tag := range query.Tags {
|
|
tagConditions = append(tagConditions, sq.Like{"tags": "%" + tag + "%"})
|
|
}
|
|
builder = builder.Where(tagConditions)
|
|
}
|
|
|
|
if query.Limit > 0 {
|
|
builder = builder.Limit(uint64(query.Limit))
|
|
}
|
|
|
|
builder = builder.
|
|
OrderBy("id ASC").
|
|
PlaceholderFormat(sq.Question)
|
|
|
|
rows, err := builder.RunWith(r.db).QueryContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var spells []models.Spell
|
|
for rows.Next() {
|
|
spell, err := scanSpell(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
spells = append(spells, spell)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return spells, nil
|
|
}
|
|
|
|
func (r *Repo) Import(ctx context.Context, ch <-chan models.Spell) error {
|
|
return r.runTx(ctx, func(tx *sql.Tx) error {
|
|
cache := sq.NewStmtCache(tx)
|
|
defer cache.Clear()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case spell, ok := <-ch:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
_, err := insertSpell(ctx, cache, spell)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (r *Repo) Count(ctx context.Context) (int, error) {
|
|
var count int
|
|
err := sq.Select("COUNT(*)").
|
|
From("spells").
|
|
RunWith(r.db).
|
|
ScanContext(ctx, &count)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
func (r *Repo) Export(ctx context.Context) (<-chan models.Spell, <-chan error, error) {
|
|
ch := make(chan models.Spell, 1)
|
|
errCh := make(chan error, 1)
|
|
rows, err := sq.Select("id", "name", "spell", "short_desc", "description", "tags").
|
|
From("spells").
|
|
RunWith(r.db).
|
|
QueryContext(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
go func() {
|
|
defer close(errCh)
|
|
defer close(ch)
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
spell, err := scanSpell(rows)
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
errCh <- ctx.Err()
|
|
return
|
|
case ch <- spell:
|
|
}
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
|
|
return ch, errCh, nil
|
|
}
|
|
|
|
func (r *Repo) runTx(ctx context.Context, f func(tx *sql.Tx) error) error {
|
|
tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin tx: %w", err)
|
|
}
|
|
|
|
if err := f(tx); err != nil {
|
|
if rbErr := tx.Rollback(); rbErr != nil {
|
|
return fmt.Errorf("err: %v, rollback err: %w", err, rbErr)
|
|
}
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func scanSpell(row scanner) (models.Spell, error) {
|
|
spell := models.Spell{}
|
|
tagsString := ""
|
|
|
|
if err := row.Scan(&spell.Id, &spell.Name, &spell.Spell, &spell.ShortDesc, &spell.Description, &tagsString); err != nil {
|
|
return models.Spell{}, err
|
|
}
|
|
|
|
if tagsString != "" {
|
|
spell.Tags = strings.Split(tagsString, ",")
|
|
}
|
|
|
|
return spell, nil
|
|
}
|
|
|
|
func insertSpell(ctx context.Context, runner sq.BaseRunner, spell models.Spell) (sql.Result, error) {
|
|
tags := strings.Join(spell.Tags, ",")
|
|
|
|
return sq.Insert("spells").
|
|
Columns("name", "spell", "short_desc", "description", "tags").
|
|
Values(spell.Name, spell.Spell, spell.ShortDesc, spell.Description, tags).
|
|
PlaceholderFormat(sq.Question).
|
|
RunWith(runner).
|
|
ExecContext(ctx)
|
|
}
|