From 4de3218c4045707bebc5849f995fee9bd4c2a644 Mon Sep 17 00:00:00 2001 From: Kirillkriv Date: Sun, 16 Aug 2026 21:54:31 +0500 Subject: [PATCH] feat: added repo export/import streaming methods --- internal/adapters/sqlite/sqlite.go | 114 +++++++++++++++++++++++++--- internal/application/application.go | 3 + 2 files changed, 108 insertions(+), 9 deletions(-) diff --git a/internal/adapters/sqlite/sqlite.go b/internal/adapters/sqlite/sqlite.go index e251f87..68ac57f 100644 --- a/internal/adapters/sqlite/sqlite.go +++ b/internal/adapters/sqlite/sqlite.go @@ -3,6 +3,7 @@ package sqlite import ( "context" "database/sql" + "fmt" sq "github.com/Masterminds/squirrel" "github.com/abelith/spellbook/internal/errors" "github.com/abelith/spellbook/internal/models" @@ -45,17 +46,10 @@ func (r *Repo) Init(ctx context.Context) error { } func (r *Repo) Add(ctx context.Context, spell models.Spell) error { - tags := strings.Join(spell.Tags, ",") - - _, err := sq.Insert("spells"). - Columns("name", "spell", "short_desc", "description", "tags"). - Values(spell.Name, spell.Spell, spell.ShortDesc, spell.Description, tags). - PlaceholderFormat(sq.Question). - RunWith(r.db). - ExecContext(ctx) - if err != nil { + if _, err := insertSpell(ctx, r.db, spell); err != nil { return err } + return nil } @@ -148,6 +142,97 @@ func (r *Repo) GetMany(ctx context.Context, query models.SearchQuery) ([]models. 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 := "" @@ -162,3 +247,14 @@ func scanSpell(row scanner) (models.Spell, error) { 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) +} diff --git a/internal/application/application.go b/internal/application/application.go index bd6fbea..813a0be 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -12,11 +12,14 @@ type SpellCaster interface { type SpellCommand interface { Add(ctx context.Context, spell models.Spell) error Delete(ctx context.Context, spellName string) error + Import(ctx context.Context, ch <-chan models.Spell) error } type SpellQuery interface { Get(ctx context.Context, spellName string) (models.Spell, error) GetMany(ctx context.Context, query models.SearchQuery) ([]models.Spell, error) + Count(ctx context.Context) (int, error) + Export(ctx context.Context) (<-chan models.Spell, <-chan error, error) } type Application struct {