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) }