diff --git a/go.mod b/go.mod index afb8afc..9b7128a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/abelith/spellbook go 1.26.0 require ( + github.com/Masterminds/squirrel v1.5.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect @@ -11,6 +12,8 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect diff --git a/go.sum b/go.sum index a511198..4cb9c33 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= @@ -5,6 +7,7 @@ github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1 github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -15,6 +18,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -33,6 +40,7 @@ github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -40,6 +48,7 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= diff --git a/internal/adapters/sqlite/sqlite.go b/internal/adapters/sqlite/sqlite.go index 95defd7..e251f87 100644 --- a/internal/adapters/sqlite/sqlite.go +++ b/internal/adapters/sqlite/sqlite.go @@ -3,6 +3,7 @@ package sqlite import ( "context" "database/sql" + sq "github.com/Masterminds/squirrel" "github.com/abelith/spellbook/internal/errors" "github.com/abelith/spellbook/internal/models" "strings" @@ -44,15 +45,14 @@ func (r *Repo) Init(ctx context.Context) error { } func (r *Repo) Add(ctx context.Context, spell models.Spell) error { - const query = `INSERT INTO spells (name, spell, short_desc, description, tags) VALUES (?, ?, ?, ?, ?)` tags := strings.Join(spell.Tags, ",") - _, err := r.db.ExecContext( - ctx, query, - spell.Name, spell.Spell, - spell.ShortDesc, spell.Description, - 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 { return err } @@ -60,53 +60,79 @@ func (r *Repo) Add(ctx context.Context, spell models.Spell) error { } func (r *Repo) Delete(ctx context.Context, spellName string) error { - const query = `DELETE FROM spells WHERE name = ?` - - if _, err := r.db.ExecContext(ctx, query, spellName); err != nil { + _, 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) { - const query = `SELECT id, name, spell, short_desc, description, tags FROM spells WHERE name = ?` - - row := r.db.QueryRowContext(ctx, query, spellName) - if err := row.Err(); err != nil { - return models.Spell{}, err - } + 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) { - sqlQuery := ` - SELECT id, name, spell, short_desc, description, tags - FROM spells - WHERE id > ?` + builder := sq.Select("id", "name", "spell", "short_desc", "description", "tags"). + From("spells") - args := []any{query.Cursor} + if query.Cursor > 0 { + builder = builder.Where(sq.Gt{"id": query.Cursor}) + } - for _, tag := range query.Tags { - if trimmed := strings.TrimSpace(tag); trimmed != "" { - sqlQuery += " AND tags LIKE ?" - args = append(args, "%"+trimmed+"%") + 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) } - sqlQuery += " ORDER BY id ASC LIMIT ?" - limit := query.Limit - if limit <= 0 { - limit = 20 + if query.Limit > 0 { + builder = builder.Limit(uint64(query.Limit)) } - args = append(args, limit) - rows, err := r.db.QueryContext(ctx, sqlQuery, args...) + 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() - spells := make([]models.Spell, 0) + var spells []models.Spell for rows.Next() { spell, err := scanSpell(rows) if err != nil { @@ -114,9 +140,11 @@ func (r *Repo) GetMany(ctx context.Context, query models.SearchQuery) ([]models. } spells = append(spells, spell) } + if err := rows.Err(); err != nil { return nil, err } + return spells, nil } diff --git a/internal/models/models.go b/internal/models/models.go index ccd7b52..96ac3c6 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -30,7 +30,11 @@ type CastResult struct { } type SearchQuery struct { - Cursor int - Limit int - Tags []string + Text string + Name string + Description string + Command string + Tags []string + Cursor int + Limit int }